# Two id families, a 23x price gap: choosing a coding model on Cloudflare

slug: workers-ai-coding-models · https://miscsubjects.com/a/workers-ai-coding-models · tags: tooling, cloudflare, workers-ai, ai-gateway, coding-agents, model-pricing · updated 2026-07-26T03:31:50.293Z

Two model ids look almost the same and are not the same product.

`@cf/moonshotai/kimi-k2.7-code` runs on Cloudflare's GPUs: published per-token price, listed in the account's model catalogue over the API, billed as Workers AI against the same Neuron allowance as an image classifier.

`moonshotai/kimi-k3` runs on Moonshot's GPUs and Cloudflare resells it: no per-token price published anywhere in the documentation, absent from the account catalogue, billed through Unified Billing against prepaid credits.

The prefix is the whole difference. Get it wrong and the request either costs twenty times what was budgeted or returns a 402.

## Evidence status

**Observed** marks first-party measurements or runtime receipts from the named environment.
**Derived** marks arithmetic calculated from cited inputs. **Specified** marks vendor or standards
documentation. **Implemented** and **deployed** name code and live-state evidence, respectively.
**Reproduced** means the stated procedure was rerun. **Externally attested** marks operator reports;
those reports show that an experience occurred, not that it is universal.

## The prefix decides the bill, the discovery path and the request shape

| | Workers AI model | Catalogue (partner) model |
| --- | --- | --- |
| Id shape | `@cf/vendor/model` | `vendor/model` |
| Examples | `@cf/moonshotai/kimi-k2.7-code`, `@cf/zai-org/glm-5.2`, `@cf/zai-org/glm-4.7-flash` | `moonshotai/kimi-k3`, `xai/grok-4.5`, `minimax/m3` |
| Who runs the GPU | Cloudflare | The model vendor |
| Billing | Workers AI, Neurons, $0.011 per 1,000 Neurons, 10,000 Neurons free per day | Unified Billing, prepaid credits, 5% fee on credit purchase, provider rates passed through |
| Per-token price published? | Yes, on the pricing page and in the models API | No — the model page links to the dashboard |
| Listed by `GET /ai/models/search`? | Yes | No |
| Needs an authenticated gateway? | No | Yes — an unauthenticated gateway answers 402 |
| Free daily allowance applies? | Yes | No |
| Anthropic Messages endpoint | Refused by name | Sometimes accepted, shape not guaranteed |

Cloudflare states the split in one sentence: "Workers AI models (models prefixed with `@cf/`) routed through AI Gateway are not charged via Unified Billing." The credit mechanics, the 5% purchase fee and the authentication requirement are covered in [Cloudflare Unified Billing](/a/cloudflare-unified-billing).

## Every coding-relevant model Cloudflare hosts, priced from the account catalogue

The list below is the account's own catalogue, not the documentation. Fetch it:

```bash
ACCOUNT_ID=<ACCOUNT_ID>            # wrangler whoami
CF_API_TOKEN=<TOKEN>               # Workers AI: Read

curl -s -H "Authorization: Bearer $CF_API_TOKEN" \
  "https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/ai/models/search?per_page=500" \
  | jq -r '.result[] | select(.task.name=="Text Generation") |
      [.name,
       (.properties[]|select(.property_id=="context_window").value),
       ((.properties[]|select(.property_id=="function_calling").value) // "no")]
      | @tsv'
```

Read on 2026-07-26: 61 models in the catalogue, 26 of them Text Generation, and **13 in the whole catalogue advertise function calling**. Prices below are the `price` property returned by that same call, in US dollars per million tokens.

| Model id | Context | Tools | Vision | Input | Cached input | Output | What it is for |
| --- | --- | --- | --- | --- | --- | --- | --- |
| `@cf/moonshotai/kimi-k2.7-code` | 262,144 | yes | yes | $0.95 | $0.19 | $4.00 | Main agent thread. The only hosted model with a large window and image input together. |
| `@cf/moonshotai/kimi-k2.6` | 262,144 | yes | yes | $0.95 | $0.16 | $4.00 | Previous Kimi. Same price, cheaper cache, no reason to pick it for new work. |
| `@cf/zai-org/glm-5.2` | 262,144 | yes | no | $1.40 | $0.26 | $4.40 | Second opinion from a different family. Most expensive hosted option. |
| `@cf/zai-org/glm-4.7-flash` | 131,072 | yes | no | $0.0605 | none | $0.40 | Background turns: titles, summaries, classification. 23x cheaper input than GLM-5.2. |
| `@cf/nvidia/nemotron-3-120b-a12b` | 256,000 | yes | no | $0.50 | none | $1.50 | Long context where output volume, not reasoning depth, dominates the bill. |
| `@cf/openai/gpt-oss-120b` | 128,000 | yes | no | $0.35 | none | $0.75 | Cheap tool calls, open licence. Window too small for a loaded agent. |
| `@cf/openai/gpt-oss-20b` | 128,000 | yes | no | $0.20 | none | $0.30 | Cheapest tool-caller with a six-figure window. |
| `@cf/google/gemma-4-26b-a4b-it` | 256,000 | yes | no | $0.10 | none | $0.30 | Large window, low price, no coding track record. |
| `@cf/qwen/qwen3-30b-a3b-fp8` | 32,768 | yes | no | $0.0509 | none | $0.335 | Cheapest tool-caller here. The window is the problem. |
| `@cf/qwen/qwen2.5-coder-32b-instruct` | 32,768 | **no** | no | $0.66 | none | $1.00 | Completion, not agency. Cannot call tools. |
| `@cf/deepseek-ai/deepseek-r1-distill-qwen-32b` | 80,000 | **no** | no | $0.497 | none | $4.881 | Cannot call tools, and the highest output price here. |

Two rows are traps. `qwen2.5-coder-32b-instruct` carries "coder" in the name and cannot call tools, so no agent can drive it. The DeepSeek distill is the same, at nine times the output price of GPT-OSS-120B. Cloudflare's [function calling](https://developers.cloudflare.com/workers-ai/features/function-calling/) page describes the capability; the catalogue is the only place that says which models have it.

## The catalogue models publish no price — the only way to learn it is to run one and read the log

`moonshotai/kimi-k3`, `xai/grok-4.5` and `minimax/m3` each have a documentation page. Each page has a Pricing row. Each Pricing row says the same thing: "View pricing in the Cloudflare dashboard".

The figures below are measured, not published: one identical request per model through the account's AI Gateway on 2026-07-26, cost read back from the gateway log rows.

| Model id | Context (docs) | Request formats (docs) | Published rate | Measured cost, this turn | Tokens in / out | Blended $/M |
| --- | --- | --- | --- | --- | --- | --- |
| `moonshotai/kimi-k3` | 1,048,576 | Chat Completions | none | $0.002283 | 126 / 127 | $9.02 |
| `xai/grok-4.5` | 500,000 | Chat Completions | none | $0.0010764 | 248 / 37 | $3.78 |
| `minimax/m3` | 1,000,000 | Chat Completions, Anthropic Messages | none | $0.00011934 | 217 / 68 | $0.42 |

A single observation cannot separate an input rate from an output rate — two unknowns, one equation. It does establish the order of magnitude: Kimi K3 costs roughly twenty times per token what MiniMax M3 costs for the same answer. Separating the two rates needs a second request with a deliberately different input-to-output ratio, then solving the pair.

## Three Cloudflare surfaces disagree about what GLM-4.7 Flash costs

Workers AI bills in Neurons and projects them into dollars. The projection is where the surfaces drift apart.

| Surface | GLM-4.7 Flash, per M input tokens |
| --- | --- |
| Pricing page, "Price in Tokens" column | $0.060 |
| Pricing page, "Price in Neurons" column | 5,500 neurons, which at $0.011 per 1,000 Neurons is $0.0605 |
| Models API `price` property | $0.0605 |
| AI Gateway log `cost` field | behaves as $0.060 |

The check that settles it: a direct Workers AI call returns a `neurons` figure in its usage block. An 18-input, 24-output turn returned `"neurons": 0.9726`, and 18 x 5,500/1e6 + 24 x 36,400/1e6 = 0.9726 exactly. Neurons are the real unit; the dollar columns are rounded projections. Meanwhile the gateway's `cost` field for a 46-input, 587-output turn came back as $0.00023756, which is 46 x $0.060 + 587 x $0.40 exactly, not 46 x $0.0605 + 587 x $0.40 = $0.000237583.

The gap is $0.000000023 on that turn. It matters because it means the log's dollar column is not authoritative to the last digit, which is worth knowing before building a chargeback report on it.

## The background slot is where a coding agent's money actually goes

A coding agent runs two model slots. The main slot answers the user. A second, smaller slot runs constantly and invisibly: naming the session, summarising the conversation when the window fills, classifying whether a command is safe. In Claude Code that slot is the environment variable `ANTHROPIC_DEFAULT_HAIKU_MODEL`. It fires whether or not anyone is watching, which is what makes a 23x input price difference compound.

Two turn shapes, priced through each model's published rate:

| Turn shape | GLM-4.7 Flash | Kimi K2.7 Code | GLM-5.2 |
| --- | --- | --- | --- |
| Measured short turn, 46 in / 587 out | $0.000238 | $0.002392 | $0.002647 |
| Conversation summary, 20,000 in / 500 out | $0.00141 | $0.02100 | $0.03020 |
| 2,000 summary turns in a month | **$2.82** | **$42.00** | **$60.40** |

The middle row written out: 20,000 x $0.0605/1,000,000 = $0.00121 input plus 500 x $0.40/1,000,000 = $0.0002 output, giving $0.00141. For GLM-5.2: 20,000 x $1.40/1,000,000 = $0.028 plus 500 x $4.40/1,000,000 = $0.0022, giving $0.0302. The 2,000-turn count is a stated assumption; the per-turn figures are not.

$57.58 a month, on turns no one reads. Point the background slot at `@cf/zai-org/glm-4.7-flash` and the main slot at whatever is worth paying for.

## How to call one

Both surfaces take a Cloudflare API token with `Workers AI: Read` and `Workers AI: Run`. Setting up the gateway itself: [How to create a Cloudflare AI Gateway](/a/cloudflare-ai-gateway-setup).

**Workers AI, OpenAI-compatible.** This is the shape almost every client expects.

```bash
curl -s -X POST \
  "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completions" \
  -H "Authorization: Bearer <TOKEN>" \
  -H "content-type: application/json" \
  -d '{"model":"@cf/zai-org/glm-4.7-flash","max_tokens":24,
       "messages":[{"role":"user","content":"What is 2+2? Answer with the number only."}]}'
```

The real response, 2026-07-26, trimmed to the fields that matter:

```json
{"model":"@cf/zai-org/glm-4.7-flash",
 "choices":[{"message":{"role":"assistant","content":null,
   "reasoning_content":"1.  **Analyze the user's request:** The user is asking for the sum of 2 and 2"},
   "finish_reason":"length"}],
 "usage":{"prompt_tokens":18,"completion_tokens":24,"total_tokens":42,"neurons":0.9726}}
```

`content` is `null`. That is not an error condition — see below.

To log, cache and rate-limit the call, add one header to the same request: `-H "cf-aig-gateway-id: <GATEWAY_ID>"`. Catalogue models use the identical path and body with the unprefixed id, for example `"model":"minimax/m3"`.

## The Anthropic endpoint refuses every `@cf/` model by name

Cloudflare exposes `POST /ai/v1/messages`. Sending a Workers AI model to it returns, verbatim:

```json
{"type":"error","error":{"type":"invalid_request_error",
 "message":"AiError: Anthropic Messages API is not supported for model \"@cf/moonshotai/kimi-k2.7-code\""}}
```

Catalogue models are accepted there, and the shape is not what the endpoint name promises. The documentation for `minimax/m3` lists its request formats as "Chat Completions, Anthropic Messages". Sent to `/ai/v1/messages` on 2026-07-26 it answered:

```json
{"id":"06b4c281ce76f4c5405e21e09af57dfa","model":"MiniMax-M3","object":"chat.completion",
 "choices":[{"message":{"role":"assistant","content":"",
   "reasoning_content":"The user simply said \"say OK\"..."},"finish_reason":"length"}],
 "usage":{"total_tokens":210,"prompt_tokens":178,"completion_tokens":32}}
```

`"object":"chat.completion"` and a `choices` array: an OpenAI body from an Anthropic-named endpoint. A client that parses `content` as an array of blocks throws on it. So a tool speaking only `POST /v1/messages` cannot reach any Workers AI model without a translator, and cannot trust the catalogue models' envelope either. That translator, with its source, is in [Claude Code on Kimi, GLM or Grok through your own Cloudflare account](/a/claude-code-on-cloudflare-ai-gateway).

## What breaks

**Reasoning models spend the output budget thinking and hand back nothing.** Kimi and GLM both emit a reasoning trace before the answer, charged as output and counted against `max_tokens`. The call above, capped at 24, returned `finish_reason: "length"`, `content: null`, and a bill for 24 output tokens. The same prompt at `max_tokens: 1024` returned `"4"` after 78 output tokens. A client reading only `content` sees an empty turn; one falling back to `reasoning_content` presents the scratchpad as the answer. Give the cap headroom.

**Constrained decoding turns that into a total failure.** Cloudflare's [JSON Mode](https://developers.cloudflare.com/workers-ai/features/json-mode/) accepts a `response_format` carrying a JSON Schema and forces the output to match it — which forbids the `<think>` preamble a reasoning model emits first. On the Workers AI binding, debuggingfuture root-caused an every-call failure to exactly this: "glm-4.7-flash answers correctly in the Cloudflare playground but failed **every** pr-review with `StructuredOutputInvalid: empty`." GLM-5.2 failed identically, which proved the decode path was at fault rather than the model, and led him to retract his own earlier "GLM is out" verdict.

Rerun on the REST path on 2026-07-26, the failure is a budget failure and it is escapable. `@cf/zai-org/glm-4.7-flash` with a two-field `json_schema` at `max_tokens: 256` returned `content: null` and 256 output tokens of reasoning. The identical request at `max_tokens: 2048` returned `{"score": 6, "why": "This code is syntactically correct and does exactly what it is supposed to do. However, it lacks context, documentation, and best practices."}` after 760 output tokens, 2,634 characters of which were discarded reasoning. Binding path and REST path do not behave the same way; the rule that covers both is to budget three to ten times the tokens the answer needs, or not to send guided JSON to a reasoning model at all.

**Corrupted output at real prompt sizes, on real traffic only.** Tenstorrent's serving stack produced garbage from Kimi K2.7 Code under an eight-thousand-token structured coding prompt: "People chatting on the console were seeing corrupted outputs. We did not observe something like this during the weekend nor with release workflow with limited samples." Short smoke tests do not test this model.

**Mid-stream failure on long agentic runs.** Faith-2002, on opencode 1.18.5, hitting Kimi K2.7 Code through a third-party host: `{"type":"error","sequence_number":1584,"code":"InternalServiceError","message":"The service encountered an unexpected internal error.","param":""}` — consistently, on complex tasks, 1,584 stream events in. Cheap tokens and reliable long runs are not the same purchase.

**Vision works or not depending on which client sends the image.** vilicvane found Kimi K2.7 Code reading images fine through one VS Code integration and rejecting them through another: "The original VSCode built-in Ollama seem to work vision of with Kimi K2.7 Code. However, when use with models provided by this extension, Kimi complains corrupted images." The catalogue's `vision: true` property is accurate; the payload shape the client builds decides whether it works.

**Announced availability is not granted entitlement.** aregtech cited GitHub's changelog announcing Kimi K2.7 for Copilot Pro, then screenshotted the CLI listing it: "The GitHub policy says that Kimi Code 2.7 (model `kimi-k2.7-code`) is available for Pro subscription. - In fact, it is listed in the `Blocked / Disabled` list." Check the catalogue call, not the announcement.

**OpenAI-compatible means Chat Completions, not everything OpenAI ships.** mrnoname set out to spend a Cloudflare Startups credit on Workers AI through Codex CLI: "Workers AI has an OpenAI-compatible API so I expected it to just work with Codex. Nope. The Responses API surface doesn't map". He wrote a proxy.

## Quality, honestly: one benchmark, one retraction, one zero

The most useful published comparison of Kimi K2.7 Code against Qwen coding models is useful mainly because its author threw his first version away. Amit Arora had published a five-model, six-task results matrix, then removed it: "The README's results section published a 5x6 matrix (Opus, Kimi, Devstral, MiniMax, Qwen Coder Next, Qwen 3.6 35B) whose per-model numbers are **not reproducible from artifacts in this repo** -- no `eval.json` files exist on disk for those models, and the figures differ materially from what the current judge produces. Publish only what we have actually measured."

What replaced it, scored by one judge (`codex exec`, `gpt-5.6-sol`, high effort), every model self-hosted on vLLM:

| Task | Kimi-K2.7-Code | Qwen3.6-35B | Qwen3-Coder-30B |
| --- | --: | --: | --: |
| remove-faiss | **75.25** | 59.25 | 49.0 |
| remove-efs | **71.25** | 63.0 | 45.0 |
| ssrf | **72.75** | 55.75 | 0.0 |
| migrate-secrets | **75.5** | 54.5 | 43.5 |
| keycloak-rds-iam | 0.0 | **48.75** | 33.25 |
| Mean of 5 | **58.95** | 56.25 | 34.15 |

The zero is the honest part. Kimi scored 0.0 on `keycloak-rds-iam`, a task Qwen3.6-35B leads, classified as a real failure rather than judging noise: it "hit the 60-turn cap with 2/4 artifacts". Qwen3-Coder-30B's zero on `ssrf` is the mirror image — it "spent every turn implementing instead of designing".

Two readings follow and they pull opposite ways. On the four tasks Kimi completed it is clearly ahead, a 73.69 mean. Across all five the margin is 58.95 to 56.25, which one blown task erases. A frontier-scale model that occasionally burns its whole turn budget and delivers half the artifacts is not strictly better than a small one that finishes.

The hardware asymmetry is stated too: "**Hardware:** Kimi-K2.7-Code (1.06T-param MoE) ran on **8x H200** (`p5en.48xlarge`); the three Qwen models (3B-active MoE) on a single **`g6e.12xlarge`** (4x L40S). All via vLLM." That is a comparison of weights, not of Cloudflare's serving of them.

On the other family the evidence is one operator changing his mind. December 2025, andai on GLM behind a coding CLI: "I had been using GLM in Claude code with Claude code router, because while you can just change the API endpoint, the web search function doesn't work, and neither does image recognition." He went back to first-party. June 2026, same person: "But it just works with Claude Code? They have a guide on their website." Between them prmph filed the other side: "For some reason I can't even get Claude Code (Running GLM 4.6) to do the simplest of tasks today without feeling like I want to tear my hair out, whereas it used to be pretty good before." Three reports, two verdicts, one direction of travel — and all three were true when written.

**The verdict.** Kimi K2.7 Code on the main thread: the only Cloudflare-hosted model with a 262,144-token window, tool calling and image input at once, leader in the only reproducible published comparison, 32% cheaper on input than GLM-5.2. GLM-4.7 Flash on the background slot. GLM-5.2 for the second opinion, not the first draft.

**What would change it.** A reproducible benchmark where Kimi's zero repeats on a second long-horizon task; a published `moonshotai/kimi-k3` rate under $2/M blended, which would make a 1M-token window affordable for the main thread; or a cached-input rate on GLM-4.7 Flash, which would make it viable for turns that repeat a large prefix.

## First-party measurement: the same coding prompt through six models

**Method.** One `POST /v1/messages` per model against the account's own Anthropic-shaped gateway route, `max_tokens: 1024`, no tools, no system prompt, single user message, sequential, one attempt each. Latency measured client-side around the `fetch`. Token counts from the response `usage` block. Cost read afterwards from the AI Gateway log rows for the same six requests, matched by timestamp and by the `model_asked` value in the request metadata. Run at 04:35:23–04:35:44 UTC on 2026-07-26.

The prompt, in full:

```
Write a Python function chunk(xs, n) that splits list xs into consecutive chunks
of length n, with a shorter final chunk if the list does not divide evenly.
Return only the code, no explanation.
```

| Requested alias | Resolved id | Latency | In | Out | Cached in | Gateway cost |
| --- | --- | --: | --: | --: | --: | --: |
| `claude-kimi-k2.7-code` | `@cf/moonshotai/kimi-k2.7-code` | 2,745 ms | 49 | 185 | 0 | $0.00078655 |
| `claude-glm-5.2` | `@cf/zai-org/glm-5.2` | 2,454 ms | 53 | 135 | 0 | $0.0006682 |
| `claude-glm-flash` | `@cf/zai-org/glm-4.7-flash` | 7,571 ms | 46 | 587 | 0 | $0.00023756 |
| `claude-kimi-k3` | `moonshotai/kimi-k3` | 6,663 ms | 126 | 127 | 0 | $0.002283 |
| `claude-grok-4.5` | `xai/grok-4.5` | 2,271 ms | 248 | 37 | 128 | $0.0010764 |
| `claude-minimax-m3` | `minimax/m3` | 1,809 ms | 217 | 68 | 114 | $0.00011934 |

**Every one of the six returned the same function.** Four wrote `xs[i:i+n]` and two wrote `xs[i:i + n]`:

```python
def chunk(xs, n):
    return [xs[i:i+n] for i in range(0, len(xs), n)]
```

**What it shows.** On a task with one obvious idiomatic answer, model choice changes nothing about the answer and a great deal about the cost. MiniMax M3 at $0.00011934 was 19 times cheaper than Kimi K3 at $0.002283 for a byte-identical result. The slowest turn came from the cheapest hosted model, GLM-4.7 Flash at 7,571 ms, because it spent 587 output tokens reasoning about a two-line function — 4.5 times the output of any other model here.

**What it does not show.** Nothing about multi-turn agentic work, tool calling, long contexts, or instruction adherence under pressure. One sample per model, one prompt, no repeats, so the latency figures carry no error bars and include whatever queueing each backend had that second. It is a floor check.

**Reconciliation.** The three Workers AI rows multiply out exactly against the published rates: 49 x $0.95 + 185 x $4.00 over a million is $0.00078655, the logged figure to the last digit; 53 x $1.40 + 135 x $4.40 is $0.0006682, likewise. A previously recorded row on this same account does *not* reconcile: a 149,187-input-token Kimi K2.7 Code turn billed $0.02852109, implying $0.191 per million — the cached rate, on a turn reporting 64 cached tokens. Published as unreconciled. The arithmetic holds at three-digit token counts and fails at six-digit ones, and the difference has not been explained.

## Choosing table

| Task | Pick | Why |
| --- | --- | --- |
| Main agent thread, tools, large repo | `@cf/moonshotai/kimi-k2.7-code` | 262,144 window, tools, vision, $0.19/M cached input, leader in the one reproducible comparison. |
| Session titles, summaries, safety classification | `@cf/zai-org/glm-4.7-flash` | $0.0605/M input against $1.40 for GLM-5.2. Same job, 23x less. |
| Second opinion on a design, different family | `@cf/zai-org/glm-5.2` | Independent weights. 47% more input, 10% more output. |
| Reading a screenshot or a diagram | `@cf/moonshotai/kimi-k2.7-code` | The only `@cf/` coding model with `vision: true`. |
| Context over 262,144 tokens | `moonshotai/kimi-k3` or `minimax/m3` | 1,048,576 and 1,000,000 tokens, both catalogue-billed and unpriced in the docs. |
| Cheapest possible tool call, small window fine | `@cf/openai/gpt-oss-20b` | $0.20/M in, $0.30/M out, 128,000 window, tools yes. |
| Bulk structured extraction with a JSON schema | `@cf/openai/gpt-oss-120b` | Guided JSON on a reasoning model burns the output budget. Budget generously. |
| Plain code completion, no agency | `@cf/qwen/qwen2.5-coder-32b-instruct` | Cheaper per output token than Kimi. Cannot drive a loop. |

## Symptom, cause, fix

| Symptom | Cause | Fix |
| --- | --- | --- |
| `"content": null`, `finish_reason: "length"`, tokens still billed | Reasoning trace consumed the whole `max_tokens` budget | Raise `max_tokens`. 1,024 is the floor for a one-line GLM-4.7 Flash answer. |
| `StructuredOutputInvalid: empty` on every guided-JSON call | `response_format` schema forbids the `<think>` preamble the model emits first | Drop `response_format` and parse JSON yourself, or give 3–10x the tokens the object needs. |
| `AiError: Anthropic Messages API is not supported for model "@cf/..."` | Workers AI models are excluded from `/ai/v1/messages` | Use `/ai/v1/chat/completions`, or put a translator in front. |
| Response has `"object":"chat.completion"` from `/ai/v1/messages` | A catalogue model returned its native OpenAI body through the Anthropic-named path | Detect the envelope shape at runtime; do not trust the endpoint name. |
| HTTP 402, "Gateway authentication is required to use unified billing" | A catalogue model routed through an unauthenticated gateway | Enable authentication on the gateway and send `cf-aig-authorization`. See [Cloudflare Unified Billing](/a/cloudflare-unified-billing). |
| `{"code":"InternalServiceError"}` mid-stream on a long run | Hosted-path instability on extended agentic sessions | Retry with backoff, resume from the last completed tool result. The stream is not atomic. |
| Garbled tokens in the answer at large prompt sizes | Serving-stack corruption that short samples do not surface | Test at the prompt size actually used. |
| "corrupted images" from a vision-capable model | Client built the image part in a shape the backend does not accept | Send OpenAI `image_url` parts on `/ai/v1/chat/completions`. Verify with a tiny known PNG. |
| Model is announced but the CLI lists it blocked | Announcement and entitlement are separate systems | Call `/ai/models/search` and treat its output as the truth. |
| The id 404s or silently resolves to another model | The public model name is not the Cloudflare id | Copy the id from the catalogue call. `@cf/moonshotai/kimi-k2.5` exists but is Deprecated. |
| Codex CLI or any Responses-API client cannot connect | Workers AI implements Chat Completions, not the Responses API | Use a Chat Completions client, or a translating proxy. |
| Cost report does not tie out to the published rate | The gateway `cost` field rounds, and large-context rows have not reconciled | Reconcile on token counts from `usage`, not the dollar column. |

## Rerun any of it

The alias-to-id mapping used in the six-model run is the `CATALOGUE` table in [the gateway translator source](https://github.com/redacted/claude-code-cloudflare-gateway). Every price and capability above comes from one call; every cost from another.

```bash
# The catalogue: prices, context windows, function_calling, vision
curl -s -H "Authorization: Bearer <TOKEN>" \
 "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/models/search?per_page=500" | jq .

# What a turn actually cost, from the gateway log (needs AI Gateway: Read, not Workers AI)
curl -s -H "Authorization: Bearer <TOKEN>" \
 "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai-gateway/gateways/<GATEWAY_ID>/logs?per_page=10&order_by=created_at&order_by_direction=desc" \
 | jq -r '.result[] | [.model, .tokens_in, .tokens_out, .cost] | @tsv'
```

## Sources

1. Workers AI pricing — https://developers.cloudflare.com/workers-ai/platform/pricing/
2. Unified Billing — https://developers.cloudflare.com/ai-gateway/features/unified-billing/
3. Cloudflare AI model catalog — https://developers.cloudflare.com/ai/models/
4. Kimi K3 (Moonshot AI) model page — https://developers.cloudflare.com/ai/models/moonshotai/kimi-k3/
5. MiniMax M3 model page — https://developers.cloudflare.com/ai/models/minimax/m3/
6. Grok 4.5 (xAI) model page — https://developers.cloudflare.com/ai/models/xai/grok-4.5/
7. JSON Mode — https://developers.cloudflare.com/workers-ai/features/json-mode/
8. Function calling — https://developers.cloudflare.com/workers-ai/features/function-calling/
9. Cloudflare API reference: AI models list — https://developers.cloudflare.com/api/resources/ai/subresources/models/methods/list/
10. AI Gateway REST API — https://developers.cloudflare.com/ai-gateway/usage/rest-api/
11. OpenAI compatible API endpoints — https://developers.cloudflare.com/workers-ai/configuration/open-ai-compatibility/
12. claude-code-cloudflare-gateway — https://github.com/redacted/claude-code-cloudflare-gateway
13. README: publish self-hosted results (Kimi-K2.7-Code + 3 Qwen models) — https://github.com/aarora79/agentic-coding-harness-benchmarks/pull/10
14. fix(review-agent): stop sending guided-JSON by default — it breaks GLM on the Workers AI binding — https://github.com/OpenHackersClub/flare-dispatch/pull/213
15. [Kimi-K2.7-Code] Corrupted Outputs — https://github.com/tenstorrent/tt-inference-server/issues/4441
16. Internal Service Error in kimi-k2.7-code — https://github.com/anomalyco/opencode/issues/38813
17. Having some trouble with vision support for Kimi K2.7 Code — https://github.com/ollama/ollama-vscode/issues/9
18. Kimi K2.7 Code is not available in Pro subscription — https://github.com/github/copilot-cli/issues/4029
19. Show HN: Codex Workers AI Proxy – Use Cloudflare Workers AI models in Codex CLI — https://news.ycombinator.com/item?id=47739925
20. Comment on "GLM-4.7: Advancing the Coding Capability" — GLM in Claude Code — https://news.ycombinator.com/item?id=46366013
21. Comment — z.ai GLM behind Claude Code via a bashrc alias — https://news.ycombinator.com/item?id=48568587
22. Comment — Claude Code running GLM 4.6 can't do simple tasks — https://news.ycombinator.com/item?id=46082971
23. Account model catalogue read on 2026-07-26 — https://miscsubjects.com/api/articles/workers-ai-coding-models
24. Six models, one coding prompt, measured latency, tokens and cost — https://miscsubjects.com/api/articles/workers-ai-coding-models
25. Reasoning budget, guided JSON and the Anthropic-endpoint refusal — https://miscsubjects.com/api/articles/workers-ai-coding-models
26. Neuron reconciliation and the row that does not reconcile — https://miscsubjects.com/api/articles/workers-ai-coding-models


---

# Cloudflare Unified Billing: the 5% is on the credits, and the 402 is one gateway toggle

slug: cloudflare-unified-billing · https://miscsubjects.com/a/cloudflare-unified-billing · tags: tooling, cloudflare, billing, ai-gateway, unified-billing, llm-routing · updated 2026-07-26T03:31:40.158Z

Cloudflare Unified Billing is a way of paying for model inference in which Cloudflare, not you, holds the credentials for OpenAI, Anthropic, Google AI Studio, Google Vertex AI, xAI and Groq. You load dollar credits onto your Cloudflare account, send an ordinary HTTPS request to `api.cloudflare.com` carrying one Cloudflare API token, name a model as `provider/model`, and Cloudflare authenticates to the upstream provider, pays them, and deducts the cost from your credit balance. No `OPENAI_API_KEY` or `ANTHROPIC_API_KEY` exists anywhere in the request path. Cloudflare's words: "Both deduct credits from your account automatically without requiring provider API keys."

## Evidence status

**Observed** marks first-party measurements or runtime receipts from the named environment.
**Derived** marks arithmetic calculated from cited inputs. **Specified** marks vendor or standards
documentation. **Implemented** and **deployed** name code and live-state evidence, respectively.
**Reproduced** means the stated procedure was rerun. **Externally attested** marks operator reports;
those reports show that an experience occurred, not that it is universal.

## Terms this page uses

| Term | Meaning |
| --- | --- |
| Unified Billing | Cloudflare authenticates and pays the provider; you pay Cloudflare. Appears as `wholesale: true` in the gateway API and log rows. |
| BYOK | Bring Your Own Keys — your provider key is stored in Cloudflare Secrets Store and forwarded; the provider bills you. |
| AI Gateway | The proxy in front of the model call: logging, caching, retries, rate limits, spend limits. Free on all plans. Setup: [/a/cloudflare-ai-gateway-setup](/a/cloudflare-ai-gateway-setup). |
| Authenticated gateway | A gateway with its `authentication` setting on, which then demands a Cloudflare API token on every request. |
| Credits | Prepaid dollars on the Cloudflare account. Bought at a 5% surcharge; spent at the provider's own per-token rate. |
| Neurons | The unit Workers AI bills in, $0.011 per 1,000. A separate ledger from credits. |

## The 5% lands on the money, not on the traffic

Cloudflare states it in one sentence: "A 5% fee is applied to all credits purchased through Unified Billing. For example, a $100 credit purchase will result in a $105 charge. Inference pricing from providers is passed through with no markup — you pay the same per-token rates as you would directly with the provider."

That is a surcharge at top-up, not a per-request markup. Nothing you change inside a request moves it, because every dollar you eventually spend on tokens cost $1.05 to acquire.

Two public statements about that number disagree; both are printed here rather than reconciled away. On Hacker News, **yencabulator** corrected a commenter who had called Cloudflare's gateway the free option: "Free? They take the same 5% fee as OpenRouter does." Right about Cloudflare, imprecise about OpenRouter. OpenRouter's FAQ uses the same shape — it "charges a fee when you purchase credits" and passes provider pricing through "without any markup" — but the rate rendered on that page is **5.5% with a $0.80 minimum** for cards, 5% for crypto. On $100 that is $105.00 against $105.50; on $20, $21.00 against $21.10. Same mechanism, different price, and "the same 5%" is close rather than exact.

The number was hard to find at all, which is why it was disputed. **bm-rf**, reading the launch docs: "Not seeing any pricing info on the models[1] page. Wonder how much of a lift this is over paying providers directly. Perhaps Cloudflare is doing this at cost? Also interesting that zero data retention is not on by default". **6thbit**, same thread: "i wonder about their princing and potential markup on top of token usage?i presume they wont let you \"manage all your AI spend in one place\" for free." Both were guessing. The 5% is on the Unified Billing feature page, not on the model catalogue they were reading.

## The arithmetic for 100 million input and 20 million output tokens a month

Assumptions: one calendar month, 100,000,000 input tokens, 20,000,000 output tokens, no cached input, no batch discount, one authenticated gateway, list prices as of 2026-07-26.

`openai/gpt-4.1-mini` is $0.40 per million input and $1.60 per million output. Input 100 × $0.40 = $40.00. Output 20 × $1.60 = $32.00. Inference $72.00. BYOK pays OpenAI $72.00 and Cloudflare nothing, because AI Gateway's core features are free. Unified Billing deducts $72.00 of credits, and acquiring $72.00 of credits costs $72.00 × 1.05 = $75.60.

`anthropic/claude-sonnet-5` is $2 / $10 per million today — Anthropic footnotes it as "Introductory pricing of $2 / $10 per MTok applies to Claude Sonnet 5 through August 31, 2026" — and "$3 / input MTok $15 / output MTok" after that.

| Route | Inference | Cloudflare fee | You pay |
| --- | --- | --- | --- |
| `openai/gpt-4.1-mini`, BYOK | $72.00 | $0.00 | **$72.00** |
| `openai/gpt-4.1-mini`, Unified Billing | $72.00 | $3.60 | **$75.60** |
| `anthropic/claude-sonnet-5` intro, BYOK | $400.00 | $0.00 | **$400.00** |
| `anthropic/claude-sonnet-5` intro, Unified Billing | $400.00 | $20.00 | **$420.00** |
| `anthropic/claude-sonnet-5` after 2026-08-31, Unified Billing | $600.00 | $30.00 | **$630.00** |

Sonnet rows: 100 × $2 = $200 plus 20 × $10 = $200 gives $400; 100 × $3 = $300 plus 20 × $15 = $300 gives $600. The fee column is the inference column × 0.05 every time. That is the whole pricing model.

A Workers AI model has no row here. Cloudflare: "Workers AI models (models prefixed with `@cf/`) routed through AI Gateway are not charged via Unified Billing. These models are billed through Workers AI pricing instead" — "$0.011 per 1,000 Neurons". A mixed setup produces two line items by design. Which of those models are worth pointing a coding agent at: [/a/workers-ai-coding-models](/a/workers-ai-coding-models).

## Five things that must be true before a request bills

1. **Credits.** Dashboard → **AI Gateway** → the **Credits Available** card, top right → **Manage** → **Top-up credits** → amount → **Confirm and pay**. A payment method is required first. Optional: **Setup auto top-up credits**, with a threshold and a recharge amount.
2. **A gateway with authentication on.** That gateway's **Settings** → **Create authentication token** (a token with the required `Run` permissions, shown once) → back on the settings page, toggle **Authenticated Gateway** on. Skipping this is the 402 below.
3. **An API token.** **My Profile → API Tokens → Create Token → Create Custom Token**, with the account-scoped permissions **AI Gateway – Run** (send inference) and **AI Gateway – Read** (list gateways, read logs). Add **AI Gateway – Edit** only to change the gateway's zero-data-retention default over the API. Cloudflare's warning: "The `AI Gateway Read`, `Run`, and `Edit` permissions cannot be restricted to a single gateway — unlike R2, which supports per-bucket scoping. Any token with `AI Gateway Run` can send requests through every gateway in the account."
4. **The account id.** The 32-character hex string after `dash.cloudflare.com/` in the dashboard URL, or from `wrangler whoami`. It is the `{account_id}` path segment below.
5. **A `provider/model` id.** `openai/gpt-4.1-mini`, `anthropic/claude-sonnet-5`, `google/gemini-3-flash`, `xai/grok-3`.

```bash
curl -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completions" \
  -H "Authorization: Bearer <TOKEN>" \
  -H "cf-aig-gateway-id: default" \
  -H "Content-Type: application/json" \
  --data '{"model":"openai/gpt-4.1-mini","max_tokens":16,
           "messages":[{"role":"user","content":"Reply with the single word: ok"}]}'
```

Omitting `cf-aig-gateway-id` routes third-party requests through the account's default gateway. Workers AI `@cf/` requests always require the header.

## The 402 has one cause, and the docs table predicts the opposite

```json
{"errors":[{"message":"Gateway authentication is required to use unified billing. Enable authentication on your gateway or provide your own API key (BYOK).","code":2021}],"success":false,"result":{},"messages":[]}
```

One condition produces it: a `provider/model` request aimed at a gateway whose `authentication` is `false`. A valid token does not help. A loaded balance does not help. Below, two requests seconds apart on an account with `cloud-kernel` (`authentication: false`) and `default` (`authentication: true`) — same token, same body, same model, one header value different.

```bash
# Change only the gateway id between these two runs.
for GW in cloud-kernel default; do
  curl -s -w " <- %{http_code} on $GW\n" -X POST \
    "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completions" \
    -H "Authorization: Bearer <TOKEN>" -H "cf-aig-gateway-id: $GW" \
    -H "Content-Type: application/json" \
    --data '{"model":"openai/gpt-4.1-mini","max_tokens":16,"messages":[{"role":"user","content":"Reply with the single word: ok"}]}'
done
```

`cloud-kernel` returned **402** with the body above. `default` returned **200**: `{"id":"id-1785040573332","object":"chat.completion","model":"openai/gpt-4.1-mini","choices":[{"index":0,"message":{"role":"assistant","content":"ok"}}],"usage":{"prompt_tokens":14,"completion_tokens":1,"total_tokens":15}}`.

| Gateway `authentication` | Request | Result |
| --- | --- | --- |
| `false` | `provider/model` | **402, code 2021** |
| `false` | `@cf/vendor/model` | 200 — Workers AI is not Unified Billing |
| `true`, token present | `provider/model` | 200 |
| `true`, token absent | anything | 401 — an auth failure, not a billing failure |
| either, BYOK key stored | `provider/model` | 200, provider bills you. BYOK's own prerequisite is still an authenticated gateway. |

Two things make this expensive. The same `@cf/` call keeps working on the unauthenticated gateway, so it reads as a model problem. And the Authenticated Gateway page's own behaviour table says the opposite: for `Authentication Setting: Off` with `No header` it states "Unauthenticated gateway — Request succeeds". True for Workers AI and for BYOK; not true for Unified Billing, and nothing on that page says so.

**pemontto** filed the narrower version on Cloudflare's own repository, `cloudflare/ai` issue 548, 2026-05-30: "POST to `/ai/v1/responses` with Unified Billing auth (`Authorization: Bearer {CF_API_TOKEN}` plus `cf-aig-gateway-id: {authenticated_gateway_id}`) returns HTTP 402 even though the gateway has `authentication: true`." The report carries a full curl reproduction, confirms `authentication: true` by reading `GET /ai-gateway/gateways/{GW}`, and shows `/ai/v1/chat/completions` returning 200 on identical headers across `openai/gpt-5.4`, `gpt-5.4-mini` and `gpt-5.5`. **Status on 2026-07-26: open, zero comments.** The same shape tried here — `POST /ai/v1/responses`, `openai/gpt-4.1-mini`, authenticated gateway — returned **200** with a complete Responses envelope. The endpoint-specific 402 did not reproduce on this account and model. Both results stand; nobody has closed the issue.

## `anthropic/*` gets Anthropic's validation rules, whatever the endpoint is called

`/ai/v1/chat/completions` is an OpenAI-shaped endpoint. The shape is a translation layer, not a guarantee. When the id starts with `anthropic/`, the request lands on Anthropic's backend under Anthropic's schema — which has no `system` role inside `messages`, only a top-level `system` field ([/a/what-is-the-anthropic-messages-api](/a/what-is-the-anthropic-messages-api)).

**pmonte**, `anomalyco/opencode` issue 32951, 2026-06-19: "Cloudflare AI Gateway Unified Billing routes requests to Anthropic's backend. When the model is `anthropic/*`, the gateway applies Anthropic's validation rules, which **do not accept `role: \"system\"` inside the `messages` array**." Their client put the system prompt at `messages[0]`; every request failed.

Reproduced against `anthropic/claude-sonnet-5` on an authenticated gateway. The body that fails, and its exact answer:

```json
{"model":"anthropic/claude-sonnet-5","max_tokens":16,
 "messages":[{"role":"system","content":"You are terse."},
             {"role":"user","content":"Reply with the single word: ok"}]}
```
```
HTTP 400
{"errors":[{"message":"Model execution failed (User Input Error): Invalid value at messages[0].role: Invalid option: expected one of \"user\"|\"assistant\"","code":7003}],"success":false,"result":{},"messages":[]}
```

Two bodies work. Drop the system message and keep `/ai/v1/chat/completions`:

```json
{"model":"anthropic/claude-sonnet-5","max_tokens":16,
 "messages":[{"role":"user","content":"Reply with the single word: ok"}]}
```

Or keep the system prompt and move to `/ai/v1/messages`, Anthropic's own shape:

```bash
curl -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/messages" \
  -H "Authorization: Bearer <TOKEN>" -H "cf-aig-gateway-id: default" \
  -H "Content-Type: application/json" \
  --data '{"model":"anthropic/claude-sonnet-5","max_tokens":16,"system":"You are terse.",
           "messages":[{"role":"user","content":"Reply with the single word: ok"}]}'
```
```json
{"id":"msg_011CdQ3qJ4WRi3L5nnG3cdc3","type":"message","role":"assistant",
 "content":[{"type":"text","text":"ok"}],"model":"claude-sonnet-5","stop_reason":"end_turn",
 "usage":{"input_tokens":22,"output_tokens":4},"gatewayMetadata":{"keySource":"Unified"}}
```

`"keySource":"Unified"` is the response naming the credential that paid — the cheapest live confirmation that a call billed through credits rather than a stored key.

Prefer the second. The first succeeds and still misleads: the measured 200 from `anthropic/claude-sonnet-5` on `/ai/v1/chat/completions` was `{"index":0,"message":{"role":"assistant","refusal":null},"logprobs":null,"finish_reason":"stop"}` — **no `content` field at all** — with `usage` reported as `{"input_tokens":16,"output_tokens":4}` instead of OpenAI's `prompt_tokens`/`completion_tokens`. A client reading `choices[0].message.content` gets an empty answer and no error to explain it. This behaviour is not in the documentation.

The mismatch bites adapters from the other side too. **just-be-dev**, `withastro/flue` issue 327, 2026-06-20, stated the motive first: "have been leaning into cloudflare's AI gateway unified billing so that I don't have to provision keys for openai/anthropic directly. Ran into a bit of an issue where using anthropic's models in this way doesn't quite work." The adapter formatted every `cloudflare/...` request as OpenAI chat-completions — correct for `cloudflare/@cf/...`, wrong for `cloudflare/anthropic/claude-sonnet-4.6`.

## Model ids and endpoints decide the ledger

| Id shape | Example | Billed as | `cf-aig-gateway-id` |
| --- | --- | --- | --- |
| `provider/model` | `openai/gpt-4.1-mini`, `anthropic/claude-sonnet-5`, `google/gemini-3-flash`, `xai/grok-3` | Unified Billing credits, 5% surcharge at top-up | optional; defaults to the account default gateway |
| `@cf/vendor/model` | `@cf/moonshotai/kimi-k2.6`, `@cf/meta/llama-3.3-70b-instruct-fp8-fast` | Workers AI Neurons, $0.011 / 1,000 | **required** |
| `provider/model` with a stored key | any of the above | the provider bills you; Cloudflare bills nothing | required, gateway must be authenticated |

Four endpoints accept Unified Billing traffic: `POST /ai/run` (Cloudflare envelope, `{model, input}`), `POST /ai/v1/chat/completions` (OpenAI chat completions), `POST /ai/v1/responses` (OpenAI Responses), `POST /ai/v1/messages` (Anthropic Messages). Only the last refuses Workers AI ids outright — Cloudflare: "Workers AI models (`@cf/`) do not support this schema." `/ai/v1/responses` accepts `@cf/` ids model-by-model.

## What is covered, and the four places the documentation stops

Providers on the HTTP API: OpenAI, Anthropic, Google AI Studio, Google Vertex AI, xAI, Groq. Zero data retention is a Unified-Billing-only control — "This setting only applies to Unified Billing requests that use Cloudflare-managed credentials. It does not apply to BYOK or other AI Gateway requests" — off by default, supported for OpenAI and Anthropic only, and where a provider lacks it "AI Gateway falls back to the standard (non-ZDR) Unified Billing configuration" without saying so at request time. Set it per gateway under **Settings → Zero Data Retention (ZDR)** or per request with `cf-aig-zdr: true`. It is not logging: "ZDR does not control AI Gateway logging." Spend limits cover both lanes — "Spend limits apply to both Unified Billing requests and BYOK requests for models with known pricing" — block with `429`, and are "eventually consistent", so "a burst of concurrent requests can briefly exceed the limit before enforcement catches up."

Four silences:

- **The 402.** The Unified Billing page lists an authenticated gateway as a prerequisite and never names the status code or the error string; the Authenticated Gateway page's behaviour table contradicts it for this lane.
- **The negative balance.** In full: "In rare instances, your credit balance may go negative. If this happens, Cloudflare will charge the payment method on file for the outstanding amount. Charges occur at the beginning of each month for the previous month." No cap is documented.
- **The `cost` field's precision.** Present on every log row, the basis of spend limits, rounding unspecified.
- **The `anthropic/*` envelope on the OpenAI endpoint.** Measured above, described nowhere.

## What one billed request leaves behind

`GET /accounts/<ACCOUNT_ID>/ai-gateway/gateways/default/logs` returns the receipt for the calls made above.

| `model` | `tokens_in` / `tokens_out` | `cost` | `authentication` | `wholesale` | `byok` |
| --- | --- | --- | --- | --- | --- |
| `anthropic/claude-sonnet-5` | 22 / 4 | `0.00008400000000000001` | `true` | `true` | `null` |
| `anthropic/claude-sonnet-5` | 16 / 4 | `0.000072` | `true` | `true` | `null` |
| `openai/gpt-4.1-mini` | 14 / 2 | `0.0000088` | `true` | `true` | `null` |

`wholesale: true` marks the row as Unified Billing; `byok: null` confirms no stored key was used. Test the no-markup claim against those three numbers:

- `openai/gpt-4.1-mini` at OpenAI's published $0.40 and $1.60 per million: (14 × 0.40 + 2 × 1.60) ÷ 1,000,000 = 8.8 ÷ 1,000,000 = **$0.0000088**. The logged value, to the digit.
- `anthropic/claude-sonnet-5`, 22 in and 4 out, at Anthropic's introductory $2 and $10: (44 + 40) ÷ 1,000,000 = **$0.000084**. The logged value. At the standard $3 / $15 the same row would be $0.000126, so the log is charging the introductory rate.
- The 16-in row: (32 + 40) ÷ 1,000,000 = **$0.000072**. The logged value.

Two providers, three rows, exact agreement. No markup per request; the 5% is entirely at top-up. A fourth call, `@cf/meta/llama-3.3-70b-instruct-fp8-fast` through the **unauthenticated** gateway, returned 200 and reported `"neurons":1.529652714729309` instead of a dollar cost — 1.5297 ÷ 1,000 × $0.011 = **$0.0000168**, in the other ledger.

## Six routes, and the one to take

| Route | Take it when | Cost of taking it |
| --- | --- | --- |
| **Unified Billing** | The number of provider accounts is the problem: one token, one invoice, one place to set spend limits, no provider key in the code | 5% on credits, and a hard dependency on Cloudflare for reachability |
| **BYOK on the same gateway** | Volume is high, or a provider contract or committed-spend discount already exists that the 5% would sit on top of | Key custody returns, one bill per provider; still needs an authenticated gateway |
| **Direct provider keys, no gateway** | A single-provider service where the gateway earns nothing | No unified logs, no cross-provider spend limit, no cache, per-provider rotation |
| **Workers AI `@cf/`** | The model you want is in Cloudflare's own catalogue and you want no third-party relationship | A much smaller catalogue than the frontier providers' |
| **OpenRouter** | Breadth of model coverage matters more than price | 5.5% + $0.80 on a card top-up, and another vendor relationship |
| **Bedrock or Vertex** | Procurement, not the model, is the constraint — spend must land on an AWS or Google contract | Vertex is already reachable *through* Unified Billing, so this is only distinct when the billing relationship is the requirement |

Two merged pull requests show what people actually adopt this for, and it is not the price. **jeremyhart** rewrote a connector to drop a confusing "Provider API Key" plus separate "Gateway Token" pair for a single Cloudflare API token with AI Gateway Run permission: "Cloudflare authenticates and bills the upstream provider, so no per-provider keys are needed." **kyleboas** made the mirror-image change on a `callLLM` that had been attaching an upstream `Authorization` header on every gateway request and injecting `ANTHROPIC_API_KEY`/`OPENAI_API_KEY` into agent inputs: "Cloudflare Unified Billing lets the gateway handle upstream authentication and billing, so provider API keys should be optional when routing through the AI Gateway."

**Verdict:** route through Unified Billing until inference spend is large enough that 5% costs more than running key rotation and reconciling several provider invoices. At $72 a month the fee is $3.60 and the decision is not close. At $50,000 a month it is $2,500, and BYOK on the same gateway removes the fee while keeping every gateway feature. The break-even is an operations question, and moving between the two is a stored secret plus a gateway setting, not a rewrite.

## Symptom, cause, fix

| Symptom | Cause | Fix |
| --- | --- | --- |
| `402` + `Gateway authentication is required to use unified billing`, code `2021` | The target gateway has `authentication: false` | Gateway **Settings → Create authentication token → Authenticated Gateway** on, or point `cf-aig-gateway-id` at a gateway that already is |
| `@cf/` models work, `provider/model` returns 402 on the same gateway | Workers AI does not use Unified Billing, so the authentication setting does not affect it | Same fix. A working `@cf/` call is not evidence the gateway is configured |
| `400` + `Invalid value at messages[0].role: Invalid option: expected one of "user"\|"assistant"` | An OpenAI-shaped body with `role: "system"` sent to an `anthropic/*` model | Move the system prompt to a top-level `system` field and call `/ai/v1/messages`, or drop the system message |
| `anthropic/*` returns 200 but `choices[0].message.content` is missing | The OpenAI envelope from an Anthropic model omits `content` and uses `input_tokens`/`output_tokens` | Use `/ai/v1/messages` and read `content[0].text` |
| `404` + `Model not found: <id>` | An id that is not in the catalogue, often a stale version suffix | `anthropic/claude-sonnet-4-5` 404s on this account; `anthropic/claude-sonnet-5` does not. Check the catalogue |
| `410` + `Model has been deprecated` | A retired Workers AI model | Pick a current id from `GET /accounts/<ACCOUNT_ID>/ai/models/search` |
| `401` on a request that used to work | Missing or revoked token on an authenticated gateway | Reissue with **AI Gateway – Run**; every method needs it, `GET` included |
| `429` with spend under budget | Spend limits are eventually consistent under concurrency, or a rate-limit rule exists | Read the gateway config — a rate rule and a spend rule both return 429 |
| `405` on `GET /ai/v1/models` | That path is not a listing endpoint on the Cloudflare API | Use `GET /accounts/<ACCOUNT_ID>/ai/models/search` for `@cf/` ids and the model catalogue for `provider/model` |
| Requests bill but no logs appear | `collect_logs` off on the gateway, or `cf-aig-collect-log: false` on the request | Turn logging on in the gateway settings; billing and logging are independent |
| Balance went negative | Documented behaviour; the card on file is charged for the shortfall at the start of the next month | Configure auto top-up with a threshold above one day's spend |

## Rerun every number here

All of it was measured on 2026-07-26 against a production account with two gateways, `cloud-kernel` (`authentication: false`) and `default` (`authentication: true`), using a token with AI Gateway Run and Read. Total spend for the whole set: under two hundredths of a cent.

```bash
# 1. Ground truth for the 402 — which gateways are authenticated
curl -s "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai-gateway/gateways?per_page=50" \
  -H "Authorization: Bearer <TOKEN>" | jq '.result[] | {id, authentication, is_default}'

# 2. The 402 / 200 pair: the loop in "The 402 has one cause" above
# 3. The shape trap: the three bodies in "anthropic/* gets Anthropic's validation rules" above

# 4. The receipt, including the cost field the arithmetic is checked against
curl -s "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai-gateway/gateways/default/logs?per_page=6&order_by=created_at&order_by_direction=desc" \
  -H "Authorization: Bearer <TOKEN>" \
  | jq '.result[] | {model, tokens_in, tokens_out, cost, authentication, wholesale, byok}'
```

Substitute your own account id and token. The `cost` values will differ with the models you call; the reconciliation is the same — multiply `tokens_in` and `tokens_out` by the provider's published per-million rates and compare. A disagreement on your account is the interesting result, because that one number is what the no-markup claim rests on.

An MIT-licensed Worker that speaks the Anthropic Messages API and forwards to this surface, including the `anthropic/*` lane and the `cf-aig-gateway-id` handling above, is at [github.com/redacted/claude-code-cloudflare-gateway](https://github.com/redacted/claude-code-cloudflare-gateway) — `tools/contract-test.mjs` runs 21 wire checks, `tools/capture-gateway.mjs` logs exactly what a client sends. The build that uses it: [/a/claude-code-on-cloudflare-ai-gateway](/a/claude-code-on-cloudflare-ai-gateway).

## Sources

1. Cloudflare AI Gateway — Unified Billing — https://developers.cloudflare.com/ai-gateway/features/unified-billing/
2. Cloudflare AI Gateway — Authenticated Gateway — https://developers.cloudflare.com/ai-gateway/configuration/authentication/
3. Cloudflare AI Gateway — REST API — https://developers.cloudflare.com/ai-gateway/usage/rest-api/
4. Cloudflare AI Gateway — Pricing — https://developers.cloudflare.com/ai-gateway/reference/pricing/
5. Cloudflare AI Gateway — Spend limits — https://developers.cloudflare.com/ai-gateway/features/spend-limits/
6. Cloudflare AI Gateway — BYOK (Store Keys) — https://developers.cloudflare.com/ai-gateway/configuration/bring-your-own-keys/
7. Cloudflare Workers AI — Pricing — https://developers.cloudflare.com/workers-ai/platform/pricing/
8. OpenAI — API pricing — https://developers.openai.com/api/docs/pricing
9. OpenRouter — FAQ, what are the fees for using OpenRouter — https://openrouter.ai/docs/faq
10. Anthropic — Model overview and pricing — https://platform.claude.com/docs/en/about-claude/models/overview
11. AI Gateway REST: /ai/v1/responses rejects Unified Billing on an authenticated gateway — https://github.com/cloudflare/ai/issues/548
12. Cloudflare AI Gateway (Unified Billing) rejects `role: "system"` in messages array when proxying Anthropic models — https://github.com/anomalyco/opencode/issues/32951
13. Support anthropic models via cloudflare's unified billing — https://github.com/withastro/flue/issues/327
14. Comment on "OpenRouter raises $113M Series B" — the Cloudflare gateway is not free — https://news.ycombinator.com/item?id=48346648
15. Comment on "Cloudflare's AI Platform" — no pricing on the models page — https://news.ycombinator.com/item?id=47793121
16. Comment on "Cloudflare's AI Platform" — markup on token usage — https://news.ycombinator.com/item?id=47793207
17. Simplify Cloudflare AI Gateway to Unified Billing; fix migration guard — https://github.com/jeremyhart/claworc/pull/6
18. Allow Cloudflare Unified Billing without upstream provider keys — https://github.com/kyleboas/blob/pull/124
19. claude-code-cloudflare-gateway — a Worker that speaks Anthropic Messages and forwards to this surface — https://github.com/redacted/claude-code-cloudflare-gateway
20. Measured per-turn cost of coding traffic through the same gateway — https://miscsubjects.com/a/claude-code-on-cloudflare-ai-gateway
21. First-party: the 402 reproduced against two gateways, 2026-07-26 — https://miscsubjects.com/a/cloudflare-unified-billing
22. First-party: the anthropic/* shape trap and both working bodies, 2026-07-26 — https://miscsubjects.com/a/cloudflare-unified-billing
23. First-party: gateway log rows with the cost field, reconciled against published rates — https://miscsubjects.com/a/cloudflare-unified-billing
24. First-party: /ai/v1/responses returned 200, so issue 548 did not reproduce here — https://miscsubjects.com/a/cloudflare-unified-billing
25. First-party: a Workers AI model on the unauthenticated gateway, billed in Neurons — https://miscsubjects.com/a/cloudflare-unified-billing


---

# How to create a Cloudflare AI Gateway, with authentication on

slug: cloudflare-ai-gateway-setup · https://miscsubjects.com/a/cloudflare-ai-gateway-setup · tags: tooling, cloudflare, ai-gateway · updated 2026-07-26T03:31:34.440Z

An AI Gateway is a URL you send AI requests to instead of sending them to OpenAI, Anthropic, xAI or Cloudflare's own models directly; Cloudflare forwards the request, returns the provider's answer unchanged, and keeps a row recording the model, the token counts, an estimated cost, the duration and the status code. It is not a model, not a router that picks a model for you, and not a billing account on its own — it is a proxy with a ledger, and everything else it does (caching, retries, rate limits, spend caps) is switched on per gateway or per request.

## Evidence status

**Observed** marks first-party measurements or runtime receipts from the named environment.
**Derived** marks arithmetic calculated from cited inputs. **Specified** marks vendor or standards
documentation. **Implemented** and **deployed** name code and live-state evidence, respectively.
**Reproduced** means the stated procedure was rerun. **Externally attested** marks operator reports;
those reports show that an experience occurred, not that it is universal.

## The gateway called `default` exists before you create anything

If a request omits the gateway id, the id `default` is used, and if no gateway by that name exists it is created on the first authenticated request. The settings it is born with are not the ones most guides assume:

| Setting on the auto-created `default` gateway | Value |
| --- | --- |
| Authentication | On |
| Log collection | On |
| Caching | Off (TTL of 0) |
| Rate limiting | Off |

Caching is off, so any claim that the gateway will cut your bill by serving repeats describes a feature you have not enabled. Authentication is on, so the first thing that happens to a client which cannot send a second HTTP header is a `401`. Auto-creation applies only to the id `default`; any other id must be created first.

[[embed:source:s1]]

## Prerequisites, with the exact labels

1. **A Cloudflare account.** The free plan is enough — AI Gateway's core features cost nothing on every plan.
2. **Your account id.** Every dashboard URL is `https://dash.cloudflare.com/<ACCOUNT_ID>/...`; the 32-character hex string after the hostname is it. `wrangler whoami` prints the same value. It goes in every request URL.
3. **Where the product lives.** Sidebar → **AI** → **AI Gateway**. Direct link: `https://dash.cloudflare.com/?to=/:account/ai/ai-gateway`.
4. **An API token**, from `https://dash.cloudflare.com/profile/api-tokens` → **Create Token** → **Create Custom Token** → **Get started**. Under **Permissions** the rows read exactly `Account` · `AI Gateway` · `Read | Edit | Run`. Under **Account Resources** pick your account, then **Continue to summary** → **Create Token**. It is displayed once.

The three permissions are not interchangeable. `Read` lists gateways and reads settings and logs through the API. `Edit` creates, updates and deletes gateways, and is required to set `cache_ttl` or rate limits by API rather than by clicking. `Run` sends inference through `gateway.ai.cloudflare.com`, and is what the `cf-aig-authorization` header carries. None can be narrowed to one gateway:

> The `AI Gateway Read`, `Run`, and `Edit` permissions cannot be restricted to a single gateway — unlike R2, which supports per-bucket scoping. Any token with `AI Gateway Run` can send requests through every gateway in the account, including any configured with stored provider keys through Bring Your Own Keys (BYOK), consuming those credentials.

A leaked `Run` token is therefore an account-wide credential that can spend any provider key you have stored. Isolation between tenants means separate Cloudflare accounts, or a Worker binding instead of a URL.

[[embed:source:s2]]

## Creating one: five clicks, or one POST

**Dashboard.** **AI** → **AI Gateway** → **Create Gateway** → type a **Gateway name** (64-character limit; the name becomes the gateway id and appears in every request URL) → **Create**.

**API**, with a token carrying `AI Gateway` · `Edit`:

```bash
curl -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai-gateway/gateways" \
  -H "Authorization: Bearer <TOKEN>" \
  -H "content-type: application/json" \
  -d '{"id":"coding","cache_ttl":0,"collect_logs":true,
       "rate_limiting_interval":60,"rate_limiting_limit":120,"rate_limiting_technique":"sliding"}'
```

What comes back is the gateway's full configuration, the same object `GET /ai-gateway/gateways` returns. This is the live record for the authenticated gateway on the account measured throughout this page, account id removed:

```json
{
  "id": "default",
  "created_at": "2026-06-11 17:31:56",
  "modified_at": "2026-07-26 02:19:03",
  "rate_limiting_interval": 60,
  "rate_limiting_limit": 120,
  "rate_limiting_technique": "sliding",
  "cache_ttl": 0,
  "log_management": 10000000,
  "log_management_strategy": "DELETE_OLDEST",
  "authentication": true,
  "collect_logs": true,
  "cache_invalidate_on_update": false,
  "logpush": false,
  "wholesale": true,
  "zdr": false,
  "store_id": "",
  "is_default": true,
  "workers_ai_billing_mode": "postpaid",
  "retry_max_attempts": null,
  "retry_delay": null,
  "retry_backoff": null
}
```

Read it as a checklist. `authentication: true`: every request needs a credential. `cache_ttl: 0`: nothing is cached unless a request asks. `rate_limiting_limit: 120` with `interval: 60`, `technique: "sliding"`: no more than 120 requests in any trailing 60 seconds. `log_management: 10000000` with `DELETE_OLDEST`: the ten-millionth log evicts the first rather than stopping collection.

[[embed:source:s16]]

## Three URL shapes, and two of them are deprecated

| Shape | URL | Status | Auth |
| --- | --- | --- | --- |
| Provider-native | `https://gateway.ai.cloudflare.com/v1/<ACCOUNT_ID>/<GATEWAY_ID>/<PROVIDER>/<provider path>` | Current | Provider's own key, plus `cf-aig-authorization` when the gateway is authenticated |
| OpenAI-compatible on the gateway host | `https://gateway.ai.cloudflare.com/v1/<ACCOUNT_ID>/<GATEWAY_ID>/compat/chat/completions` | Deprecated, still works | Same |
| Cloudflare REST API | `https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completions` | Current, recommended for new work | `Authorization: Bearer <TOKEN>` |

The old **Universal Endpoint** — `POST https://gateway.ai.cloudflare.com/v1/<ACCOUNT_ID>/<GATEWAY_ID>` with an array of provider objects as the body, each carrying `provider`, `endpoint`, `headers` and `query`, tried in order as fallbacks — is deprecated too; fallback and retry work now points at Dynamic Routing.

Model support across the four REST endpoints is not uniform:

| Endpoint | Body format | Third-party models | Workers AI `@cf/` models |
| --- | --- | --- | --- |
| `POST /ai/run` | `{"model":…,"input":{…}}` | Yes | Yes, with `cf-aig-gateway-id` |
| `POST /ai/v1/chat/completions` | OpenAI chat completions | Yes | Yes, with `cf-aig-gateway-id` |
| `POST /ai/v1/responses` | OpenAI Responses | Yes | Model-dependent |
| `POST /ai/v1/messages` | Anthropic Messages | Yes | **No** |

Third-party models are named `author/model` (`openai/gpt-4.1`, `xai/grok-3`). Cloudflare-hosted models are `@cf/author/model` and reach a gateway only when the request carries `cf-aig-gateway-id`; without it the call still runs, it is simply not logged, cached or rate-limited by any gateway.

[[embed:source:s3]]

## A token the REST API accepts is refused by the provider-native host

The two hosts do not check the same thing, and the failure gives no hint.

**A. Provider-native host, authenticated gateway, no `cf-aig-authorization`:**

```bash
curl -sS -X POST "https://gateway.ai.cloudflare.com/v1/<ACCOUNT_ID>/default/workers-ai/v1/chat/completions" \
  -H "Authorization: Bearer <TOKEN>" -H 'content-type: application/json' \
  -d '{"model":"@cf/moonshotai/kimi-k2.7-code","max_tokens":8,"messages":[{"role":"user","content":"hi"}]}'
```

```json
{"success":false,"result":[],"messages":[],"error":[{"code":2009,"message":"Unauthorized"}],
 "name":"AiGatewayError","httpCode":401,"internalCode":2009,"message":"Unauthorized"}
```

**B.** The same call with `-H "cf-aig-authorization: Bearer <TOKEN>"` added returns a byte-identical body. The token was a valid Cloudflare token lacking `AI Gateway` · `Run`, so a missing header and an under-permissioned token are indistinguishable from the response. Check the header first, then the permissions.

**C. REST host, same token, same gateway, same model, no `cf-aig-authorization` at all:**

```bash
curl -sS -X POST "https://api.cloudflare.com/client/v4/accounts/<ACCOUNT_ID>/ai/v1/chat/completions" \
  -H "Authorization: Bearer <TOKEN>" -H "cf-aig-gateway-id: default" \
  -H 'content-type: application/json' \
  -d '{"model":"@cf/moonshotai/kimi-k2.7-code","max_tokens":12,"messages":[{"role":"user","content":"Say OK"}]}'
```

```json
{"id":"id-1785040709314","object":"chat.completion","created":1785040709,
 "model":"@cf/moonshotai/kimi-k2.7-code",
 "usage":{"prompt_tokens":10,"completion_tokens":12,"total_tokens":22,
          "prompt_tokens_details":{"cached_tokens":0},"neurons":5.227272987365723}}
```

HTTP 200, logged on the authenticated gateway. The rule: `AI Gateway` · `Run` is a requirement of the `gateway.ai.cloudflare.com` host, not of authenticated gateways as such.

[[embed:source:s17]]

## Authentication breaks every client whose whole config is a base URL and a key

With authentication on, a provider-native request must carry `cf-aig-authorization: Bearer <TOKEN>` alongside whatever credential the provider itself wants. Cloudflare's behaviour table:

| Authentication | Header | Result |
| --- | --- | --- |
| On | Present | Succeeds |
| On | Absent | Fails |
| Off | Present | Succeeds |
| Off | Absent | Succeeds |

A client whose entire configuration is a base URL and an API key has no slot for a second header. Cloudflare's own container-based coding agent hit exactly that:

> When Authenticated Gateway is enabled on the AI Gateway, all requests from Moltworker fail because the required `cf-aig-authorization` header is never sent.

The documented escape hatch — a Worker **binding**, pre-authenticated by account identity and needing no header — is unavailable when the calling binary lives inside a container that only received a base URL.

The neighbouring failure is the shape of the *first* header rather than the absence of the second. Anthropic's API wants `x-api-key`; a client hardcoding `Authorization: Bearer` gets a `401` that reads like a bad key:

> cc-switch currently sends `Authorization: Bearer <key>` for all upstream providers. Cloudflare AI Gateway requires `x-api-key: <anthropic_key>` instead, causing 401 errors when trying to use it as a proxy.

That project's fix: detect any base URL containing `gateway.ai.cloudflare.com`, switch to an `x-api-key` strategy, add an optional `cf-aig-authorization` from an environment variable. When choosing a client, that pair of features is what to look for.

[[embed:source:s10]]
[[embed:source:s13]]

## The `cf-aig-*` headers, their defaults, and what each changes

A request-level header always wins over the gateway setting; the gateway setting is the default when no header is sent.

| Header | Default | Observable effect |
| --- | --- | --- |
| `cf-aig-gateway-id` | none | Routes a REST API call through the named gateway. Required for `@cf/` models to be logged by a gateway at all. |
| `cf-aig-authorization` | none | `Bearer <TOKEN>` for the `gateway.ai.cloudflare.com` hosts when authentication is on. Missing or under-permissioned gives `401 code 2009`. |
| `cf-aig-metadata` | none | Up to 5 JSON entries stored on the log row, filterable in the dashboard, queryable as `metadataRaw`. |
| `cf-aig-cache-ttl` | gateway `cache_ttl`, `0` on a new gateway | Seconds to keep the response. Minimum 60, maximum one month. |
| `cf-aig-skip-cache` | `false` | `true` bypasses the cache for that request. |
| `cf-aig-cache-key` | none | Replaces the default hashed key. With no gateway caching enabled, such a response lives 5 minutes. |
| `cf-aig-max-attempts` / `cf-aig-retry-delay` / `cf-aig-backoff` | gateway retry settings, unset on a new gateway | Up to 5 attempts, delay up to 5000 ms, backoff `constant`, `linear` or `exponential`. On the last attempt the gateway waits however long the provider takes. |
| `cf-aig-request-timeout` | none | Milliseconds before the request errors or falls back. Measured from the first byte, so a slow stream does not trip it. |
| `cf-aig-collect-log` | gateway `collect_logs` | `false` drops the whole row, metrics included. |
| `cf-aig-collect-log-payload` | `true` when logging is on | `false` keeps the metrics and drops the stored request and response bodies. |
| `cf-aig-custom-cost` | none | Overrides the cost figure on the row with your negotiated rates. |
| `cf-aig-cache-status` (response) | — | `HIT` or `MISS`. See the caveat below. |
| `cf-aig-step` (response) | — | Which fallback step answered; `0` is the primary. |

[[embed:source:s4]]

## The cache key is a hash of the whole request, which is why real traffic never hits it

Cloudflare builds the key by concatenating provider, endpoint path, model, the provider authentication header and **the full request body**, then hashing with SHA-256. Any byte that moves, moves the key.

An operator who put production support-bot traffic through a gateway measured the consequence:

> I was routing support-bot traffic through Cloudflare AI Gateway and  noticed the cache hit rate was near zero despite the product being  mature.

He pulled 500 consecutive misses, stripped each to the user message, and found 89% mapped to fewer than 30 distinct intents. Request ids, trace ids, timestamps and session wrappers riding in the body moved the keys — not the questions. His answer was a Worker that canonicalises the request before the gateway sees it, with Vectorize for near-matches and a Durable Object per hot key.

The mechanism reproduces in five requests, same gateway, `cf-aig-cache-ttl: 300` on all five:

| Request | Body | Result |
| --- | --- | --- |
| 1 | `{"model":"@cf/moonshotai/kimi-k2.7-code","max_tokens":8,"messages":[{"role":"user","content":"cache probe alpha"}]}` | miss (first fill) |
| 2 | identical to 1 | **hit** |
| 3 | identical to 1 | **hit** |
| 4 | same user message, system message `requestId=<uuid>` | miss |
| 5 | same user message, system message `requestId=<uuid>` (a different uuid) | miss |

Two hits out of five, and both misses in the second pair carried a question the gateway had already answered. One 36-character field was enough.

Three things follow. Strip request ids, timestamps and session wrappers from the body, or move them into `cf-aig-metadata`, where they are logged but not hashed. Use `cf-aig-cache-key` to pin the key to the part of the request that determines the answer. Expect no semantic matching: Cloudflare says caching "applies only to identical requests" and that semantic search is future work.

One measured gap. On the `api.cloudflare.com` REST host the two cached responses came back **without** a `cf-aig-cache-status` header, though the documentation says to read that header to tell a hit from a miss. Their headers were `date`, `content-type`, `content-length`, `api-version`, `cf-ai-neurons`, `cf-auditlog-id`, `server` and `cf-ray` — no `cf-aig-*` at all — while the analytics records both as `cached: 1`. On that host the analytics is the only cache signal.

[[embed:source:s11]]
[[embed:source:s5]]
[[embed:source:s18]]

## Rate limiting is what stands between a leaked token and your balance

Because a `Run` token reaches every gateway on the account, and Unified Billing spends real credits, the gateway limit is the blast-radius control. **AI** → **AI Gateway** → your gateway → **Settings** → **Rate-limiting**, then set a count, a period, and fixed or sliding.

The two techniques differ. With ten requests per ten minutes starting at 12:00, a fixed window runs 12:00–12:10 then 12:10–12:20, so ten requests at 12:09 and ten more at 12:11 all succeed; a sliding window rejects the second batch because it counts the trailing ten minutes. Over the limit is `429 Too Many Requests` and the request is not forwarded.

The gateway measured here runs 120 per 60 seconds, sliding. Over the 30 days to 2026-07-26 that produced exactly one `429` in 5,252 requests across the account — no obstruction to normal work, and a cap of 172,800 requests a day on a runaway loop instead of no cap at all. Unified Billing adds a ceiling you cannot raise: 200 requests per 60 seconds per gateway.

[[embed:source:s6]]

## Logs keep the prompt and the completion, and they are how you audit spend

A row carries the user prompt, the model response, the provider, the timestamp, the status, token usage, cost, duration and the client's user agent. Bodies are stored unless the request sends `cf-aig-collect-log-payload: false`, which keeps the metrics and drops the text.

| Limit | Value |
| --- | --- |
| Logs stored, Workers Paid | 10,000,000 per gateway |
| Logs stored, Workers Free | 100,000 across all gateways |
| Size of one log | 10 MB |
| Log write rate | 500 per second per gateway |
| Custom metadata | 5 entries per request |
| Cacheable request size | 25 MB |
| Gateways per account | 10 free, 20 paid |

Retention is a count, not a clock: logs are kept until the gateway hits its storage limit, then either the oldest are deleted (`log_management_strategy: "DELETE_OLDEST"`) or new logs stop being saved.

Two ways to read them without the dashboard. The Logs API, `GET /accounts/<ACCOUNT_ID>/ai-gateway/gateways/<GATEWAY_ID>/logs`, needs `AI Gateway` · `Read`. The GraphQL dataset `aiGatewayRequestsAdaptiveGroups` needs only account analytics access and produced every measurement here:

```bash
curl -sS -X POST https://api.cloudflare.com/client/v4/graphql \
  -H "Authorization: Bearer <TOKEN>" -H 'content-type: application/json' \
  -d '{"query":"query { viewer { accounts(filter:{accountTag:\"<ACCOUNT_ID>\"}) {
        aiGatewayRequestsAdaptiveGroups(limit:20, filter:{date_geq:\"2026-07-26\", gateway:\"default\"}) {
          count dimensions { datetimeMinute model provider statusCode cached durationMs
                             tokensIn tokensOut cost } } } } }"}'
```

Six real rows from that gateway:

| Time (UTC) | Model | Provider | Status | Cached | Duration ms | Tokens in | Tokens out | Cost USD |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| 2026-07-26T04:35 | `@cf/zai-org/glm-4.7-flash` | workers-ai | 200 | 0 | 7413 | 46 | 587 | 0.00023756 |
| 2026-07-26T04:35 | `@cf/zai-org/glm-5.2` | workers-ai | 200 | 0 | 2335 | 53 | 135 | 0.0006682 |
| 2026-07-26T04:35 | `minimax/m3` | minimax | 200 | 0 | 1145 | 217 | 68 | 0.00011934 |
| 2026-07-26T04:36 | `anthropic/claude-sonnet-4-5` | unknown | 500 | 0 | 677 | 0 | 0 | 0 |
| 2026-07-26T04:37 | `anthropic/claude-sonnet-5` | anthropic | 200 | 0 | 406 | 16 | 4 | 0.000072 |
| 2026-07-26T04:37 | `anthropic/claude-sonnet-5` | unknown | 400 | 0 | 316 | 0 | 0 | 0 |

Read the failures too. A model id that is not on the catalogue produces `provider: "unknown"`, a `500` and a cost of zero — an attempt logged that never reached a provider. The `400` on the last line is the same shape.

Metadata is stored verbatim, so send something useful. From the same gateway, session identifier removed:

```json
{"via":"claude-code","shim":"api/aig","model_asked":"kimi","session":"<REDACTED>","tools":9}
```

That is what makes cost attributable per agent instead of per account.

[[embed:source:s7]]
[[embed:source:s14]]

## What it costs, and the one number people report as wrong

The gateway is free on every plan: dashboard analytics, caching and rate limiting are core features at no charge, and persistent logs are included within the storage limits above. Logpush is Workers Paid only — 10 million requests a month, then $0.05 per million. Unified Billing adds 5% to credit purchases, so a $100 top-up is charged as $105, while per-token inference is the provider's own rate with no markup.

Measured spend on the authenticated gateway, 30 days to 2026-07-26: 107 requests, 651,894 input tokens, 8,342 output tokens, **$0.44830454**. The second gateway on the same account carried the bulk: 5,145 requests, $32.19921439. Both are the gateway's own cost column summed by GraphQL.

That column is where the complaint sits. Cloudflare labels it an estimate:

> The cost metric is an **estimation** based on the number of tokens sent and received in requests. While this metric can help you monitor and predict cost trends, refer to your provider's dashboard for the most **accurate** cost details.

A customer running production traffic on flagship image models says it is not merely imprecise:

> because Cloudflare AI Gateway is reporting inaccurate/wrong price for flagship models such as Nano Banana 2 and Nano Banana pro (I run production app using those). Been reporting it on discord and twitter, and they don't care.

Both can be true, and together they set the rule: use the cost column for trends and per-agent attribution, reconcile against the provider's invoice before billing anyone, and if you have negotiated rates send `cf-aig-custom-cost` so the column reflects your contract rather than list price.

[[embed:source:s8]]
[[embed:source:s9]]

## The hop costs less than the noise

No Cloudflare-published overhead figure exists; the circulating numbers are community estimates of roughly 10 to 50 ms, and cross-vendor benchmarks are not comparable because "almost every one of these benchmarks clocks proxy forwarding against a mock upstream, which removes the one variable that dominates real requests: the model provider's own response time." Measured here with one model, one prompt, `max_tokens: 1`, `cf-aig-skip-cache: true`, six `curl -w "%{time_total}"` runs each way, the only difference being the `cf-aig-gateway-id` header: median **0.741 s** through the gateway against **0.648 s** without, both arms spanning roughly 0.5 to 1.3 seconds. The 93 ms gap sits inside run-to-run variance at n=6 — evidence the hop is too small to separate from jitter, not evidence of a 93 ms tax.

[[embed:source:s15]]

## Where the traffic egresses is a failure mode nobody writes down

Cloudflare answers from the point of presence nearest the caller, and the provider sees the request coming from there. If the provider blocks a country Cloudflare has a PoP in, requests fail non-deterministically depending on where they land:

> this made Cloudflare's AI Gateway an unusable product, as they hilariously put a node in HK so you never know when your Anthropic request is randomly going to fail defeating the whole purpose behind the product.

No gateway setting pins egress. The mitigations are the REST API host, which terminates on Cloudflare's API rather than the anycast gateway edge; Unified Billing, so the provider sees Cloudflare as the customer; or a Worker with fixed placement calling the provider directly.

Against that, for people whose providers are not geo-blocking them the setup really is small:

> We used cloudflare's AI gateway which is pretty simple. Set one up, get the proxy URL and set it through the env var, very plug-and-play

Both reports concern the same product a year apart and neither cancels the other. The gateway is one environment variable of work and a real geographic gamble on Anthropic traffic.

[[embed:source:s12]]
[[embed:source:s19]]

## Symptom, cause, fix

| Symptom | Cause | Fix |
| --- | --- | --- |
| `402` with `{"errors":[{"message":"Gateway authentication is required to use unified billing. Enable authentication on your gateway or provide your own API key (BYOK).","code":2021}]}` | A third-party model routed through a gateway with `authentication: false`. `@cf/` models on the same gateway keep working, which disguises it. | Turn **Authenticated Gateway** on, or supply your own provider key. |
| `401` with `{"code":2009,"message":"Unauthorized"}` from `gateway.ai.cloudflare.com` | Either no `cf-aig-authorization`, or a token without `AI Gateway` · `Run`. The body is identical either way. | Add the header; if it is already there, re-mint the token with `Run`. |
| `401` from a client that worked against Anthropic yesterday | The client sends `Authorization: Bearer` where the Anthropic-native path wants `x-api-key`. | Use a client that switches header shape on `gateway.ai.cloudflare.com`, or send `x-api-key` yourself. |
| A custom provider request reaches `/v1/<path>` upstream when `base_url` has no `/v1` | Reported against `cloudflare/ai`: with `base_url https://api.exa.ai` and slug `exa-provider`, `POST …/custom-exa-provider/search` arrived at `/v1/search`. | Until it is fixed, set `base_url` so the prepended `/v1` lands on the real path, and verify against the upstream's access log. |
| A dynamic route to a Workers AI model returns `400` on the universal endpoint but `200` on `/compat/chat/completions` | Reported against `ai-gateway-provider`: same model, prompt and OpenAI-shaped body, two outcomes; reproduced with a raw universal-endpoint request, so it is not the SDK serialiser. | Send dynamic Workers AI routes to `/compat/chat/completions`, or move to the REST API host. |
| Anthropic requests fail at random and succeed on retry | The request egressed from a PoP in a region Anthropic blocks. | Use the `api.cloudflare.com` REST host, or Unified Billing, rather than the anycast gateway host. |
| `429` | Your gateway rate limit, your spend limit, or the 200-per-60s Unified Billing ceiling. | Raise the limit, wait out the window, or split traffic across gateways. |
| Zero cache hits | `cache_ttl` is `0` on a new gateway, or the body carries a value that changes per request. | Set a TTL, and strip or relocate ids and timestamps. |
| No rows in **Logs** | `collect_logs` off, `cf-aig-collect-log: false`, storage limit reached with `DELETE_OLDEST` off, or a `@cf/` model called on the REST API without `cf-aig-gateway-id`. | Check the gateway settings, then the header. |

[[embed:source:s20]]
[[embed:source:s21]]

## Setup ends where billing and model choice begin

Credits, the 5% fee and what Unified Billing does and does not cover: [Cloudflare Unified Billing](/a/cloudflare-unified-billing). Which `@cf/` models are worth pointing a coding agent at and what they cost: [Workers AI coding models](/a/workers-ai-coding-models). Wiring a coding agent to a gateway end to end, including non-Anthropic models behind an Anthropic-shaped client: [Claude Code on Kimi, GLM or Grok through your own Cloudflare account](/a/claude-code-on-cloudflare-ai-gateway).


## Sources

1. Cloudflare AI Gateway — Get started — https://developers.cloudflare.com/ai-gateway/get-started/
2. Cloudflare AI Gateway — Authentication — https://developers.cloudflare.com/ai-gateway/configuration/authentication/
3. Cloudflare AI Gateway — REST API — https://developers.cloudflare.com/ai-gateway/usage/rest-api/
4. Cloudflare AI Gateway — Header glossary — https://developers.cloudflare.com/ai-gateway/glossary/
5. Cloudflare AI Gateway — Caching — https://developers.cloudflare.com/ai-gateway/features/caching/
6. Cloudflare AI Gateway — Rate limiting — https://developers.cloudflare.com/ai-gateway/features/rate-limiting/
7. Cloudflare AI Gateway — Logging — https://developers.cloudflare.com/ai-gateway/observability/logging/
8. Cloudflare AI Gateway — Costs — https://developers.cloudflare.com/ai-gateway/observability/costs/
9. Comment on Cloudflare's AI Platform — https://news.ycombinator.com/item?id=47806253
10. Authenticated AI Gateway support in Moltworker — https://github.com/cloudflare/moltworker/issues/74
11. Comment on AI Gateway cache hit rate — https://news.ycombinator.com/item?id=47301851
12. Comment on AI Gateway geographic egress — https://news.ycombinator.com/item?id=42862073
13. Cloudflare AI Gateway authentication support in cc-switch — https://github.com/farion1231/cc-switch/issues/2311
14. Cloudflare GraphQL Analytics API — https://developers.cloudflare.com/analytics/graphql-api/
15. Gateway latency A/B probe — https://miscsubjects.com/a/cloudflare-ai-gateway-setup
16. Live gateway configuration and 30-day account measurement — https://developers.cloudflare.com/api/resources/ai_gateway/methods/create/
17. Gateway-host versus REST-host authentication probe — https://developers.cloudflare.com/ai-gateway/configuration/authentication/
18. Five-request exact-cache probe — https://developers.cloudflare.com/ai-gateway/features/caching/
19. Comment on AI Gateway setup — https://news.ycombinator.com/item?id=46100225
20. Custom provider endpoint prepends /v1 — https://github.com/cloudflare/ai/issues/476
21. Dynamic Workers AI route fails on universal endpoint — https://github.com/cloudflare/ai/issues/617

