wrap() detects Router from the client’s base URL. No other code change.
Two things about Router shape how it bills. The model you asked for is not necessarily the model that answered, so the SDK bills the one the response reports. And Router bills its own rate card, published per model in GET /v1/models for your account, so price mode reads that catalog rather than a public listing.
Live path
Wrap an OpenAI or Anthropic client pointed at
api.router.com. Bills as calls happen, priced from Router’s own catalog.No backfill path
Router exposes no programmatic usage surface. Usage lives in its dashboard Logs view only.
Live path
Router has no client of its own. You wrap an OpenAI or Anthropic client pointed at Router, and the endpoint is what marks the call as Router-served.
Use the Anthropic client for Anthropic models. Router’s Messages surface reports prompt-cache writes with their 5-minute and 1-hour split, and price mode bills each at its own catalog rate. The Responses surface does not report Anthropic cache writes, so those tokens can only be billed as input there. Either client accepts any model in your catalog, so this is a recommendation rather than a constraint.
In both cases
api_key is your Router key, read here from RAMP_ROUTER_API_KEY, and model ids come from GET /v1/models. Router’s ids are account-specific.
OpenAI client
Anthropic client
api.router.com, or any .router.com subdomain for a regional or staging endpoint. Both wrappers run the same check. The base-URL rules that identify Databricks and Snowflake still run first, so those keep their own provider.
Everything on the OpenAI page still applies to the OpenAI client, and everything on the Anthropic page to the Anthropic one: streaming, async, per-call overrides. Streaming bills once, from the terminal usage event.
The served model is the one billed
Router does not always answer with the model you named. Amodels fallback list sends no model field at all, and Switchyard routing can substitute one model for another. So the SDK bills the model the response reports, which means Router-served traffic rolls up in Lago against the same model name a direct call would report.
A fallback list replaces model rather than accompanying it. Sending both is a 400.
Candidates are the catalog’s
provider:model ids, the router.catalog_id on each GET /v1/models entry, not the id you would pass to model. A bare model name is rejected with models entry 0 must be a provider:model string.Attribution
Attribution works as it does anywhere else in the SDK:subscription at wrap time, extra_lago={"subscription": "sub_acme"} per call in Python, or lago: { subscription: "sub_acme" } in TypeScript.
Separately, put the same id in Router’s own metadata field. Router stores it with its usage record and shows it in the request detail.
Price mode
Inpricing_mode="price", a Router call is priced from Router’s own catalog: the per-model rates in GET /v1/models for your account, which are the rates Router bills. Each entry carries rates for input, output, cache reads and cache writes, with separate 5-minute and 1-hour write rates where the vendor distinguishes them. Cost events carry price_source: "ramp_router".
No extra configuration
The catalog is account-scoped and requires your Router key.wrap() reads the key from the client you pass in and loads the catalog in the background before the first call, so the setup above is complete. The key is sent only to api.router.com.
Set
ramp_router_api_key only to price Router usage without calling wrap(), for example when you build a CanonicalUsage yourself and pass it to emit(). An explicit value always wins over a learned one.The served model is what is looked up
Router answers with the vendor’s own name for the model: a dated snapshot such asgpt-5.4-nano-2026-03-17 or claude-haiku-4-5-20251001, or a Fireworks path such as accounts/fireworks/models/.... The SDK resolves each of those back to its catalog entry by exact name, alias, or version suffix. A name that two entries claim at different rates is left unpriced rather than guessed.
Only the default tier is priced
Router reports the service tier each call was served at. Thedefault tier bills at the catalog rate. flex and priority bill at rates the catalog does not publish, so the SDK does not price them. A call served at either tier emits token events and reports a PricingUnavailableError through on_error that names the tier:
service_tier to default in the request. Anthropic models do not accept a pinned tier; leave the field out and they serve the default. A response that reports no tier bills at the catalog rate.
Requests served with your own provider key
When Router serves a request with a provider key you brought, Router does not charge for it and the vendor bills you directly. The response is identical either way, so the SDK bills the catalog rate in both cases. The catalog rate matches what the vendor charges for the same model, so the amount is right and only the payee differs. Router falls back to its shared key when yours fails, and bills normally then.Models Router bills off its own catalog
At the time of writing, Router bills a small set of OpenAI models at a fixed multiple of the rate its catalog publishes. The SDK bills the published rate and applies no correction, because the catalog is Router’s to maintain and a factor built into the SDK would be wrong for every customer the day the catalog changes. To match Router’s dashboard on these models, apply the multiple as amarkup and remove it once the catalog is updated.
markup on LagoConfig does the same thing globally. See Markup.
Calls that fall back to token counts
Beyond the tier rule above, a Router call falls back to token events with aPricingUnavailableError when:
- The catalog has not loaded yet on the very first call, or the model is not in your catalog.
- The entry was served through a different backend than the one whose rate the catalog publishes. Some Fireworks entries also carry a Baseten alias (
deepseek-ai/...,zai-org/...,moonshotai/...). A call served through Baseten bills Baseten’s rate, which the catalog does not publish, and the response names the model by that alias. The SDK leaves those unpriced rather than billing the wrong rate. - The model is
gpt-3.5-turboorgpt-4. Router serves them asgpt-3.5-turbo-0125andgpt-4-0613, a snapshot suffix the SDK does not resolve.
What gets captured
Events are stampedprovider: "ramp_router" on both surfaces, and api says which surface answered. The two are stamped differently because they report tokens under different conventions.
On
/v1/responses, read from the Responses usage object:
Audio and tool-call fields map as they do on any Responses call. See OpenAI for the full table.
On
/v1/messages, fields are read from the Messages usage object exactly as on the Anthropic page: input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens and its ephemeral_5m_input_tokens / ephemeral_1h_input_tokens split, all additive. Non-Anthropic models served on this surface arrive in the same shape, with thinking tokens inside output_tokens.
Three keys can land in extras:
extras is diagnostic and is not sent to Lago. These keys are visible to a debug or error hook, but they do not appear on your events and cannot be used to split a charge. To bill differently by service tier, set that up in your Lago plan.Known limits
There is no backfill path. Router’s only routes areGET /v1/models, POST /v1/responses, POST /v1/messages and POST /v1/messages/count_tokens. Usage lives in the dashboard’s Logs view, which displays tokens, cost, service tier and metadata without offering any way to fetch them. Traffic the wrapped client never saw is not recoverable.
Nothing is skipped as a gateway cache hit, because Router has no response cache. Provider prompt caching does pass through, and those cache-read and cache-write tokens are billed like any other.
A proxy in front of Router is not detected. Reaching Router through your own hostname bills as plain OpenAI or plain Anthropic, with the wrong provider, the wrong price table and an unparsed model id. Point the wrapped client at Router directly.
api.router.com sits behind bot management. A rejected client can get an HTML challenge page rather than Router’s documented JSON error envelope. The SDK degrades to zero usage rather than throwing, either way.
Failures never bill. Every documented Router status emits nothing, including 402 insufficient_credits, 429 rate_limit_exceeded and 502 all_candidates_failed, as does a response reporting zero usage.
Next steps
Billing
How price mode works, what a cost event carries, and the token-mode plan setup that keeps subset and additive fields apart.
Configuration reference
Every config knob, in both SDKs.