Skip to main content
Cloudflare AI Gateway is a proxy, not a provider. It sits in front of OpenAI, Anthropic, Mistral, Gemini, and Cloudflare’s own Workers AI, and it caches, logs, and meters everything passing through. That changes two things for billing. Some responses never reach the provider at all, so they cost nothing and must not be billed. And Cloudflare already meters the real cost of every call it forwards, which is more accurate than any price a third party can compute. The SDK handles both, through two paths you can use separately or together:

Live path

Point your existing wrapped client at the gateway. Bills as calls happen, and skips Cloudflare’s own cache hits on OpenAI and Anthropic.

Backfill path

Poll the gateway’s Logs API and bill Cloudflare’s own metered cost. Pass each entry’s id and re-running a window does not double-bill.

Live path

There is no Cloudflare-specific client to wrap. You wrap the same provider client as always, pointed at your gateway’s base URL instead of the provider’s.
Everything on the Anthropic, OpenAI, Gemini, and Mistral pages still applies. Two behaviors are layered on top.
Some provider SDKs take the gateway auth header per call rather than at construction. The Mistral client, for example, wants http_headers={"cf-aig-authorization": ...} on chat.complete, and takes server_url rather than base_url.

Gateway cache hits are not billed, on two of the five clients

When the gateway serves a response from its own cache, it sets cf-aig-cache-status: HIT and never calls the provider. Nothing was spent, so the SDK emits nothing for that response. This is implemented in the OpenAI and Anthropic wrappers only, and only for non-streaming calls. Reading that header means going through the provider SDK’s raw-response accessor rather than the plain create call. In Python, .with_raw_response.create(...) then .parse(), which returns the identical object, so nothing downstream changes. With no gateway in the path the header is simply absent, making this a no-op on direct provider calls. Streaming is excluded deliberately: it would need .with_streaming_response, which behaves differently and is not verified end to end. See Known limits for the full per-client picture.

Workers AI is priced from Cloudflare’s own catalog

Workers AI is Cloudflare’s own inference, reached through the gateway’s OpenAI-compatible /compat endpoint. Wrap an OpenAI-shaped client at that endpoint and use workers-ai/@cf/... model ids:
An OpenAI-shaped client can point at real OpenAI or at Workers AI, and the client type alone cannot tell you which. The SDK resolves it from the model string the response reports: an id starting with @cf/ is Workers AI’s naming convention and never a real OpenAI model, so those events are stamped provider: "workers-ai" and priced from Cloudflare’s catalog rather than OpenRouter. That distinction is worth real money, not just correctness. Cloudflare’s catalog is the rate the gateway actually bills at. OpenRouter lists other hosts’ prices for the same open-weight models, and on a live check its figure for one model came out around 3.5x lower than what Cloudflare charged. Pricing Workers AI off OpenRouter would not be a naming mismatch, it would be the wrong number.
Workers AI pricing needs credentials. OpenRouter and the AWS price list are public. Cloudflare’s model catalog is not, so price mode needs a Cloudflare account id and an API token:
Without both set, Workers AI prices are unavailable and the SDK falls back to token-count events, the same as any other price miss.The catalog is read for three units: input tokens, output tokens, and cached input tokens. A Workers AI model priced per image or per audio-minute, or with no price published at all, is absent from the table and falls back to token events too.
The catalog fetch is primed the moment you call wrap() on a client pointed at gateway.ai.cloudflare.com, not at SDK init, and it runs on the background thread. wrap() normally happens well before your first completion, so the table is usually warm before it lands.

Backfill path

For usage that already happened, do not replay calls. Read the gateway’s own logs. Cloudflare reports a real metered cost per entry, so there is no price lookup on our side at all. This lives in a separate namespace from the wrapper adapters, because there is no client to instrument here. It is meant to run as a poller.
The logs endpoint is GET https://api.cloudflare.com/client/v4/accounts/{account_id}/ai-gateway/gateways/{gateway_id}/logs, paginated, authenticated with a Cloudflare API token. The list and single-entry endpoints return the same entry shape. Because Cloudflare hands over one lump sum per call rather than a per-token-type split, this path emits a single llm_cost event with no token_type, and unit set to input plus output tokens. Splitting that lump proportionally would substitute a guess for the exact number you came here for. Live-path price mode, which does have per-token unit prices, emits one event per priced type instead. Both work with the same sum_agg metric on field_name: "unit" and a dynamic charge.
You do not need to filter cache hits out. Cloudflare reports a gateway cache hit with tokens_in: 0, tokens_out: 0, and cost: 0, so a cached entry extracts as zero usage and bills zero on its own. Billing policy never has to branch on the cached flag. The flag is exposed in extras for your own reporting, not because the maths needs it.

Two new emit() arguments

The backfill path is built on two arguments that price mode gained for exactly this case:
Scope the idempotency key by subscription. transaction_id is unique across your whole Lago organization, not per subscription. If you backfill a window onto one subscription and later re-run it onto a different one, a bare cf_{entry_id} key collides with the ids already used and the events silently go nowhere.Always include the subscription in the key, as in f"cf_{sub}_{entry['id']}".
Unified or per-call attribution. If all of a gateway’s traffic belongs to one customer, bill every entry to a single subscription and ignore per-call attribution. If one gateway serves several customers, use resolve_subscription(entry) and fall back to a default only for unattributed entries. Vary the event_id prefix between the two strategies so a switch does not collide with keys already spent.

Attributing a log entry to a subscription

resolve_subscription() reads lago_subscription out of the entry’s metadata, which is populated from the cf-aig-metadata header on the original request. Set that header at call time and every log entry carries its own Lago subscription:
It returns None when the header was never set. Deciding what to do with an unattributed entry (drop it, warn, bill a default) is left to you, deliberately.

What gets captured

From a Logs API entry: Events are tagged api: "cloudflare_gateway". The entry’s cached flag, step, and id land in CanonicalUsage.extras (as cached, step, and log_id), because the poller needs them: cached to decide whether Cloudflare served the request for free, log_id as the replay key. Cloudflare names some providers differently from the SDK, so a few are mapped on the way in: google-ai-studio, google-vertex-ai, and vertex all become gemini; azure-openai and azureopenai become openai; workersai becomes workers-ai. Anything unrecognized passes through unchanged, which means it will miss on price and fall back to token events rather than being billed against the wrong rate card. Bedrock is deliberately left unmapped for that reason.
Unlike the provider-native adapters, this one never has to guess which model actually served a request. A Cloudflare log entry always reports the resolved model, so it is immune by construction to the alias-versus-snapshot mismatch that affects request-side model ids.
Malformed or missing fields degrade to zero rather than raising. One bad entry in a batch does not take down a whole poller run.

Choosing a path

Only the backfill path bills Cloudflare’s metered cost. The live path never sees it: no response header carries a per-call cost, so price mode there works exactly as it does for a direct provider call, off the public rate cards.
You can run both, but only over different traffic. The two paths share no idempotency key: a live event gets a random transaction_id, a backfilled one gets the event_id you pass. Lago treats them as unrelated events and bills the same call twice. Use the live path for current traffic and the backfill path only for windows the live path never covered.

Known limits

Cache-hit skipping covers OpenAI and Anthropic clients only, and only when not streaming. Reading cf-aig-cache-status means going through the provider SDK’s raw-response accessor, which the streaming path does not expose. An older or custom OpenAI client without with_raw_response also falls back to the plain path with no detection. If you rely on gateway caching for cost control, use the backfill path — it handles cache hits for every provider, streaming included, because Cloudflare logs them with zero tokens and zero cost. Cloudflare’s metered cost excludes reasoning tokens. It is exact on input, output, cache reads, and cache writes, but additive reasoning tokens are left out. On two measured thinking-heavy Gemini calls the reported cost came to roughly 4% of what Google actually charged — 22.8× and 39.6× under. For models that do not reason, the backfill figure is the one to bill on; for a thinking-heavy workload, meter it in token mode and price the reasoning tokens yourself. Cloudflare does not normalize usage_metadata key casing. It passes through whatever convention the underlying provider used: Anthropic and OpenAI entries come back snake_case (input_cached_tokens), while a captured Gemini entry used camelCase (reasoningTokens). The adapter checks both forms for every field it maps, but that is observed behavior across two providers, not a documented Cloudflare guarantee — a provider using a third convention could report tokens the adapter reads as zero. If you add a provider to your gateway, verify one log entry against the counter in Lago before trusting the rollup.

Next steps

Configuration reference

Every config knob, in both SDKs.

Bill in dollars

How price mode and the llm_cost metric work.