Skip to content

Latest commit

 

History

6 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Claude Code on the Cloudflare AI Gateway

Run the Claude Code CLI against Kimi, GLM, Grok, MiniMax or Claude, with every request going through your own Cloudflare account — one bill, one log, no provider API keys in your shell.

Cloudflare publishes a first-party Claude Code integration, but it covers the anthropic provider only: Claude Code speaks the Anthropic Messages API (POST /v1/messages) and every other model in the Cloudflare catalogue is Chat Completions. This Worker is the missing translation layer, in one file, deployed on your account.

Measured on claude-cli 2.1.165, 2026-07-25. Full write-up with sources: https://miscsubjects.com/a/claude-code-on-cloudflare-ai-gateway

claude ──▶ POST /v1/messages ──▶ this Worker ──▶ your AI Gateway ──▶ @cf/moonshotai/kimi-k2.7-code
                                                                  ├▶ @cf/zai-org/glm-5.2
                                                                  ├▶ moonshotai/kimi-k3   (1M ctx)
                                                                  ├▶ xai/grok-4.5
                                                                  ├▶ minimax/m3
                                                                  └▶ anthropic/claude-opus-5

Setup

1. Create an API token at https://dash.cloudflare.com/profile/api-tokens with Workers AI: Read + Workers AI: Run and AI Gateway: Run on your account.

2. Make sure your gateway has authentication ON. Unified Billing is refused on an unauthenticated gateway — catalogue models return HTTP 402 with Gateway authentication is required to use unified billing while @cf/… models keep working, which reads like a model problem and is a gateway setting.

3. Deploy.

git clone https://github.com/massoumicyrus/claude-code-cloudflare-gateway
cd claude-code-cloudflare-gateway
npx wrangler deploy                       # creates the Worker; secrets need it to exist
npx wrangler secret put CF_ACCOUNT_ID     # your account id
npx wrangler secret put CF_API_TOKEN      # the token from step 1
npx wrangler secret put SHIM_TOKEN        # any random string, e.g. openssl rand -base64 24
npx wrangler secret put AIG_RUN_TOKEN     # optional: gateway Run token for cf-aig-authorization

Deploy first. wrangler secret put against a Worker that does not exist yet prompts to create one interactively and fails outright in a non-interactive shell.

Two more optional secrets the Worker reads:

Secret What it does Default
AIG_GATEWAY_ID Which AI Gateway the requests are logged and billed through. Must be an authenticated gateway for Unified Billing models to work. default
DEFAULT_MODEL The model any unresolved slot falls back to — an empty model, or a Claude-shaped name that matches no alias and no keyword. @cf/moonshotai/kimi-k2.7-code

4. Point the CLI at it.

export ANTHROPIC_BASE_URL="https://claude-code-cloudflare-gateway.<subdomain>.workers.dev/<SHIM_TOKEN>"
export ANTHROPIC_AUTH_TOKEN="<SHIM_TOKEN>"
export ANTHROPIC_API_KEY=""
export ANTHROPIC_MODEL="kimi"
export ANTHROPIC_DEFAULT_OPUS_MODEL="kimi"
export ANTHROPIC_DEFAULT_SONNET_MODEL="kimi"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="glm-flash"   # the background/title slot
export CLAUDE_CODE_SUBAGENT_MODEL="kimi"
export ENABLE_TOOL_SEARCH=true                     # see "The two settings that matter"
export CLAUDE_CODE_ATTRIBUTION_HEADER=0
claude

The SHIM_TOKEN is both the path segment and the accepted bearer, so it works whether or not your client sends ANTHROPIC_AUTH_TOKEN (an existing /login session can override that variable).

Persist it instead in ~/.claude/settings.json:

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://<worker-host>/<SHIM_TOKEN>",
    "ANTHROPIC_AUTH_TOKEN": "<SHIM_TOKEN>",
    "ANTHROPIC_MODEL": "claude-kimi-k2.7-code",
    "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-kimi-k2.7-code",
    "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-kimi-k2.7-code",
    "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-glm-flash",
    "CLAUDE_CODE_SUBAGENT_MODEL": "claude-kimi-k2.7-code",
    "ENABLE_TOOL_SEARCH": "true",
    "CLAUDE_CODE_ATTRIBUTION_HEADER": "0",
    "DISABLE_TELEMETRY": "1",
    "API_TIMEOUT_MS": "1200000"
  }
}

Why claude-kimi-k2.7-code and not kimi? The desktop client validates model names against an Anthropic-shaped allowlist and rejects kimi-k2.7-code before sending anything. Any name containing kimi, glm, grok or gpt resolves to that model here, so a Claude-shaped name routes wherever you want.

Models

Alias Resolves to Billing
kimi @cf/moonshotai/kimi-k2.7-code (262k ctx, tool calling) Workers AI
kimi-k2.6 @cf/moonshotai/kimi-k2.6 Workers AI
kimi-k3 moonshotai/kimi-k3 (1,048,576 ctx) Unified Billing
glm @cf/zai-org/glm-5.2 Workers AI
glm-flash @cf/zai-org/glm-4.7-flash (cheapest tool-caller) Workers AI
grok xai/grok-4.5 Unified Billing
gpt openai/gpt-5.5 Unified Billing
minimax minimax/m3 Unified Billing
opus5 / sonnet5 anthropic/claude-opus-5 / -sonnet-5 Unified Billing

Any @cf/... id or author/model id passes through unchanged. anthropic/* takes the native lane (/ai/v1/messages, body untouched, no translation loss); everything else is translated over /ai/v1/chat/completions.

The two settings that matter more than the model

Both measured through one gateway with a 856-tool MCP server attached:

Configuration Input tokens / turn Cached input Cost / turn
MCP attached, no tool search 149,187 64 $0.02852109
MCP attached, ENABLE_TOOL_SEARCH=true 14,109 12,480 $0.00443075
MCP server disabled mid-session 21,928 13,312 $0.01089448
Protocol only, no MCP server 14,071 $0.00456265
  • ENABLE_TOOL_SEARCH=true drops the request from 856 tool definitions to 9 plus a ToolSearch tool the model calls on demand. Verified end to end: Kimi searched for an MCP tool it had never been shown and invoked it correctly. Tool schemas, not the conversation, are what a big MCP setup actually costs.
  • CLAUDE_CODE_ATTRIBUTION_HEADER=0 stops the client prepending a per-request nonce (x-anthropic-billing-header: … cch=…) to the system prompt. api.anthropic.com strips that line positionally; everyone else caches nothing behind it. This Worker also strips it server-side, which is why cached input goes from 64 tokens to ~13,000.

What the translation gets right

Seven things, each of which is a real bug in some published shim:

  1. Streams. A gateway that buffers whole responses stalls the client.
  2. Emits tool arguments incrementally (content_block_startinput_json_delta), not one blob at the end.
  3. Orders tool results correctly: Anthropic puts tool_result in a user turn, OpenAI wants role:"tool" messages straight after the assistant call.
  4. Maps stop reasons (stopend_turn, lengthmax_tokens, tool_callstool_use).
  5. Never returns an empty turn: Kimi and GLM spend the output budget on reasoning_content first, and an empty assistant turn makes the CLI abort with "no visible output". The reasoning text is used as a fallback.
  6. Answers /v1/messages/count_tokens with an estimate instead of a 404.
  7. Drops the attribution block without reordering the system array — the strip is positional, and a merged block starting with that header swallows the rest of your system prompt.

Verify a deployment

Requires Node 18 or newer.

node tools/contract-test.mjs https://<worker-host>/<SHIM_TOKEN> kimi

42 checks over the parts the CLI actually depends on: the Anthropic envelope, usage and stop-reason mapping, the full SSE event sequence in order with every content block closed before message_delta and usage.output_tokens on it, a streamed tool_use block whose arguments arrive as input_json_delta and parse to valid JSON, the same tool call non-streamed, a tool_result second turn the model reads, count_tokens, the model list with every published id actually resolving to the model its display_name names, that a wrong token and an unauthenticated GET /v1/models are both refused while x-api-key and Authorization: Bearer both work, and the error statuses (400 on a non-JSON body, 404 on an unknown path, 405 on PUT/DELETE). Every check passes on the reference deployment as of 2026-07-25.

What else it serves

  • GET /v1/models — the alias list in the shape gateway model discovery accepts. That discovery is off by default (CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY=1, CLI ≥ 2.1.129), times out at three seconds, treats any redirect as failure, and drops every id that does not start with claude or anthropic — which is why the ids here are claude-kimi-k2.7-code, claude-glm-5.2 and so on, with the real model in display_name.
  • Images. image blocks are forwarded as OpenAI image_url parts, base64 or URL. Verified: an 8×8 blue PNG pasted through the CLI came back "Blue" from @cf/moonshotai/kimi-k2.7-code.
  • Per-session cost attribution. cf-aig-metadata carries the CLI's x-claude-code-session-id, x-claude-code-agent-id and parent agent id, plus the model you asked for and the tool count, so the gateway's cost view is filterable per session and per subagent instead of one blur.
  • Gateway-side retries. cf-aig-max-attempts: 3 with exponential backoff, because the client's own retry logic matches on Anthropic's error wording and will not fire for an upstream that phrases failures differently.

Wire capture tool

tools/capture-gateway.mjs is a local Anthropic-compatible server that logs exactly what the client sends and answers with a valid response, so you can re-derive all of this on a newer CLI version instead of trusting this README:

node tools/capture-gateway.mjs   # listens on :8787, appends ./capture.jsonl (override with CAPTURE_LOG)
ANTHROPIC_BASE_URL=http://localhost:8787 ANTHROPIC_AUTH_TOKEN=x claude -p "say ok"

Reproducible three-way benchmark contract

tools/benchmark.mjs is the scorer and artifact writer used by the V2 comparison. It refuses a run if the per-row MCP, deferred Tool Search and protocol-only trials differ on model, gateway, catalogue snapshot or environment hash. A trial passes only when its final value is correct and every task-required lifecycle event occurred; a plausible final answer without the required resolve/invoke/receipt path fails.

Adapters receive one bounded JSON request on stdin and return one JSON result. The harness writes raw trials.jsonl, machine-readable summary.json and human-readable summary.md, including full-task success, calls, latency, input/output tokens and cost supplied by the adapter. Its cost wording treats deferred Tool Search and protocol-only as effectively equal when their mean cost is within 5%; it does not turn a rounding difference into a winner.

node --test tools/benchmark.test.mjs

The fixed tasks, real-model adapter and release results live with the portable catalogue runtime so they can exercise create, resolve, invoke, receipt, replay and repair on a clean local install.

Known limits

  • Anthropic "doesn't endorse, maintain, or audit third-party gateway products, and doesn't support routing Claude Code to non-Claude models through any gateway." Nothing forbids it; you are the one who keeps up when the client changes.
  • thinking is dropped rather than translated. That is deliberate — the client sends thinking: {"type":"adaptive"} and many backends 400, empty out or hang on it.
  • Prompt caching is whatever the upstream does. cache_control breakpoints are not honoured by Workers AI models; the cached-input rates are prefix-based, which is why stripping the nonce matters.
  • The model picker will not list these models: gateway model discovery is off by default and ignores any id that does not start with claude or anthropic.
  • AI Gateway token permissions cannot be scoped to one gateway. A Run token reaches every gateway on the account, including any holding stored provider keys.

License

MIT.

About

Run Claude Code against Kimi, GLM, Grok or Claude through your own Cloudflare AI Gateway. One Worker, one bill, no provider keys.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages