1import * as Effect from "effect/Effect";
2import type * as Redacted from "effect/Redacted";
3import type { InputProps } from "../../Input.ts";
4import * as Namespace from "../../Namespace.ts";
5import * as Output from "../../Output.ts";
6import { Secret } from "../SecretsStore/Secret.ts";
7import { GatewayProvider } from "./GatewayProvider.ts";
8
9export interface ProviderKeyProps {
10 /**
11 * The AI Gateway the provider key belongs to. The gateway must have its
12 * `storeId` set to a Secrets Store id — Cloudflare resolves the key inside
13 * that store. Changing the gateway triggers a replacement.
14 */
15 gatewayId: string;
16 /**
17 * The upstream provider the key authenticates against (e.g. `openai`,
18 * `anthropic`, `workers-ai`). Changing the provider triggers a
19 * replacement.
20 */
21 providerSlug: string;
22 /**
23 * The Secrets Store attached to the AI Gateway via `storeId`.
24 */
25 store: {
26 storeId: string;
27 accountId: string;
28 };
29 /**
30 * The provider API key. Stored in Cloudflare Secrets Store and never bound
31 * into the Worker runtime.
32 */
33 value: Redacted.Redacted<string>;
34 /**
35 * Alias distinguishing multiple keys for the same provider. Changing the
36 * alias renames the backing secret, replacing it and the provider config.
37 * @default "default"
38 */
39 alias?: string;
40 /**
41 * Optional free-form description on the Secrets Store secret.
42 */
43 comment?: string;
44 /**
45 * Whether this key is the gateway's default credential for the provider
46 * (used when a request does not name a specific key).
47 * @default false
48 */
49 defaultConfig?: boolean;
50 /**
51 * Maximum number of requests allowed per `rateLimitPeriod` through this
52 * key. Omit for no limit. Changing the limit triggers a replacement
53 * (Cloudflare exposes no update API for provider configs).
54 */
55 rateLimit?: number;
56 /**
57 * The rate limit window in seconds.
58 * @default 60
59 */
60 rateLimitPeriod?: number;
61}
62
63export type ProviderKey = {
64 /**
65 * The Secrets Store secret holding the provider API key, named
66 * `{gatewayId}_{providerSlug}_{alias}` and scoped to `ai_gateway`.
67 */
68 readonly secret: Secret;
69 /**
70 * The gateway's BYOK provider config referencing {@link secret}.
71 */
72 readonly gatewayProvider: GatewayProvider;
73};
74
75/**
76 * Declares a Cloudflare AI Gateway BYOK provider key.
77 *
78 * Cloudflare requires BYOK secrets to live in the gateway's attached Secrets
79 * Store, be scoped to `ai_gateway`, and use the exact
80 * `{gatewayId}_{providerSlug}_{alias}` name. This helper keeps that naming
81 * contract with the {@link GatewayProvider} declaration so app stacks do not
82 * have to wire the secret and provider config manually.
83 *
84 * The children are namespaced under the given id: a {@link Secret} (child
85 * `Secret`) holding the key, and a {@link GatewayProvider} (child `Provider`)
86 * referencing it. It returns `{ secret, gatewayProvider }` so either
87 * underlying resource stays addressable.
88 *
89 * Rotating `value` updates the secret in place. Changing `alias` (or
90 * `providerSlug`) renames the secret — a replacement — and cascades: the
91 * provider config is replaced and re-pointed at the new secret.
92 *
93 * ### Bringing your own key
94 * **Example:** Bring your own OpenAI key
95 * ```typescript
96 * const store = yield* Cloudflare.SecretsStore.Store("Store");
97 *
98 * const gateway = yield* Cloudflare.AI.Gateway("Gateway", {
99 * id: "my-gateway",
100 * storeId: store.storeId,
101 * });
102 *
103 * const { secret, gatewayProvider } = yield* Cloudflare.AI.ProviderKey("OpenAiKey", {
104 * store,
105 * gatewayId: gateway.gatewayId,
106 * providerSlug: "openai",
107 * value: yield* Config.Redacted("OPENAI_API_KEY"),
108 * });
109 * ```
110 *
111 * **Example:** Multiple keys for one provider
112 * Distinguish keys for the same provider with an `alias` — each alias gets
113 * its own secret and provider config.
114 * ```typescript
115 * const production = yield* Cloudflare.AI.ProviderKey("OpenAiKey", {
116 * store,
117 * gatewayId: gateway.gatewayId,
118 * providerSlug: "openai",
119 * value: yield* Config.Redacted("OPENAI_API_KEY"),
120 * });
121 *
122 * const evals = yield* Cloudflare.AI.ProviderKey("OpenAiEvalsKey", {
123 * store,
124 * gatewayId: gateway.gatewayId,
125 * providerSlug: "openai",
126 * alias: "evals",
127 * value: yield* Config.Redacted("OPENAI_EVALS_API_KEY"),
128 * });
129 * ```
130 *
131 * @see https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/
132 *
133 * @resource
134 * @product AI Gateway
135 * @category AI
136 */
137export const ProviderKey = (id: string, props: InputProps<ProviderKeyProps>) =>
138 Effect.gen(function* () {
139 const alias = props.alias ?? "default";
140 const secret = yield* Secret("Secret", {
141 store: props.store,
142 name: Output.interpolate`${props.gatewayId}_${props.providerSlug}_${alias}`,
143 value: props.value,
144 scopes: ["ai_gateway"],
145 comment: props.comment,
146 });
147
148 const gatewayProvider = yield* GatewayProvider("Provider", {
149 gatewayId: props.gatewayId,
150 providerSlug: props.providerSlug,
151 alias,
152 secretId: secret.secretId,
153 defaultConfig: props.defaultConfig,
154 rateLimit: props.rateLimit,
155 rateLimitPeriod: props.rateLimitPeriod,
156 });
157
158 return {
159 secret,
160 gatewayProvider,
161 } satisfies ProviderKey;
162 }).pipe(Namespace.push(id));