Skip to content

feat(cloudflare/ai-gateway): AiGatewayProviderKey — declarative BYOK provider keys#586

Open
agcty wants to merge 1 commit into
alchemy-run:mainfrom
agcty:feat/cloudflare-ai-gateway-provider-keys
Open

feat(cloudflare/ai-gateway): AiGatewayProviderKey — declarative BYOK provider keys#586
agcty wants to merge 1 commit into
alchemy-run:mainfrom
agcty:feat/cloudflare-ai-gateway-provider-keys

Conversation

@agcty

@agcty agcty commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Motivation

AI gateways are commonly deployed per stage, but BYOK provider keys (OpenAI, Anthropic, Google Vertex, Bedrock, ...) are a manual dashboard step per gateway today, which breaks fully-declarative deploys. Cloudflare made BYOK API-configurable, powered by Secrets Store (BYOK docs, changelog).

API research

Cloudflare's BYOK contract has two halves:

  1. A Secrets Store secret named {gateway_id}_{provider_slug}_{alias} with the ai_gateway scope. AI Gateway resolves keys by name at request time; the docs are explicit that secret_id is not used for runtime lookup, so API-created secrets must follow the convention.
  2. A provider config on the gateway (/accounts/{account_id}/ai-gateway/gateways/{gateway_id}/provider_configs): POST creates the row, GET lists rows, and DELETE {id} removes a row. The documented PUT {id} endpoint only rotates the secret value; this resource rotates by patching the named Secrets Store secret instead, because runtime lookup is by name.

Requests then drop provider Authorization headers entirely; a non-default key is selected per request with cf-aig-byok-alias.

Design

One resource, Cloudflare.AiGatewayProviderKey, owning both remote objects, mirroring how AiGatewaySpendingLimit owns the limit + top-up config pair:

const store = yield* Cloudflare.SecretsStore("Store");
const gateway = yield* Cloudflare.AiGateway("Gateway", {
  storeId: store.storeId,
});

const openaiKey = yield* Cloudflare.AiGatewayProviderKey("OpenAIKey", {
  gateway,
  store,
  provider: "openai",
  value: yield* Config.redacted("OPENAI_API_KEY"),
});
  • Why not a thin StoreSecret specialization: the naming convention alone is not the full contract; the docs instruct creating the provider configuration as well, and the config row carries default_config and per-key rate limits. The resource follows the documented API flow: create the named secret scoped to ai_gateway, then create the config row referencing it via secret_id.
  • Gateway/store relationship: the gateway must be configured with the same Secrets Store via storeId; the resource validates this up front so Cloudflare does not fail later with a misleading provider-config secret lookup error.
  • Identity & replacement: the secret name is the key's identity, so gateway/store/provider/alias changes replace; diff mirrors StoreSecret's redacted value comparison for rotation.
  • Rotation: a value change PATCHes the named secret in place. Gateway traffic picks it up through the name-based lookup, so the provider-config PUT {id} rotation endpoint is intentionally not used.
  • defaultConfig / rate limits: no in-place update exists, so structural drift is converged by recreating the config row. The secret is untouched, so other key configs are not disrupted.
  • Reconcile is one observe -> ensure -> sync flow per the reconciler doctrine: secret get-by-id -> name scan -> create tolerating SecretNameAlreadyExists -> unconditional PATCH; config list-scan -> recreate-on-drift -> create with bounded retry for Secrets Store propagation.
  • read brands name-matched configs Unowned because provider configs expose no ownership signal, same as StoreSecret.
  • store is an explicit prop like StoreSecret's, keeping the dependency graph visible.

Dependency

This builds against the embedded @distilled.cloud/cloudflare 0.25.1 workspace. That version already contains the API surface this resource uses: optional secretId on createProviderConfig and deleteProviderConfig for destroy/recreate. The updateProviderConfig endpoint from alchemy-run/distilled#336 is not required by this design.

Validation

  • bun run build from packages/alchemy passes against embedded Distilled 0.25.1.
  • Live focused test passed with a real Cloudflare account/API token: CLOUDFLARE_ACCOUNT_ID=... CLOUDFLARE_API_TOKEN=... bun run test test/Cloudflare/AiGateway/AiGatewayProviderKey.test.ts.
  • The live test covers create -> verify provider config + named secret -> rotate secret value + recreate config for rate-limit drift -> destroy, plus alias-change replacement and cleanup of the old config/secret.

🤖 Generated with Claude Code

Comment thread packages/alchemy/src/Cloudflare/AiGateway/AiGatewayProviderKey.ts Outdated
@agcty agcty force-pushed the feat/cloudflare-ai-gateway-provider-keys branch from 07189a4 to 5909b1e Compare June 16, 2026 17:27
@agcty agcty marked this pull request as ready for review June 16, 2026 17:27
@agcty agcty force-pushed the feat/cloudflare-ai-gateway-provider-keys branch from 5909b1e to 4354af0 Compare June 16, 2026 17:29
…keys)

Declarative BYOK provider keys for AI Gateway. The resource owns both
halves of Cloudflare's BYOK contract: the Secrets Store secret named
per the {gatewayId}_{provider}_{alias} lookup convention (scoped to
ai_gateway) and the gateway provider config that activates it.

- Rotation: a value change patches the named secret in place — runtime
  lookup is by name, so traffic picks up the new key immediately.
- defaultConfig / rate limits: immutable in place on the API, converged
  by recreating the config row (the secret is untouched).
- provider / alias / gateway / store changes replace the resource (the
  secret name is the identity).
- read brands name-matched configs Unowned (no ownership signal).

Depends on @distilled.cloud/cloudflare gaining updateProviderConfig /
deleteProviderConfig and optional secret/secretId on create
(alchemy-run/distilled#336).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@agcty agcty force-pushed the feat/cloudflare-ai-gateway-provider-keys branch from 4354af0 to 8b62a28 Compare June 16, 2026 17:31
@agcty

agcty commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@sam-goodwin rebased and tested with cf resources now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants