wrap() detects it from the client’s base URL. No other code change.
Two things about Router change how it bills. The model you asked for is not necessarily the model that answered, so the request is not a source of truth. And Router’s own rate card cannot be looked up, so there is nothing to price against.
Live path
Wrap an OpenAI client pointed at
api.router.com. Bills as calls happen.No backfill path
Router exposes no programmatic usage surface. Usage lives in its dashboard Logs view only.
Live path
There is no Ramp Router client to wrap. You wrapOpenAI pointed at Router, and the endpoint is what marks the call as Router-served.
api.router.com, or any .router.com suffix for a regional or staging endpoint. A substring test would also match https://evil.example.com/api.router.com/v1 and stamp an unrelated endpoint’s traffic as Router-served. The base-URL path table that identifies Databricks and Snowflake still runs first, so those keep their own provider.
Everything on the OpenAI page still applies: streaming, async, per-call overrides.
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.
What gets captured
Events are stampedprovider: "ramp_router", 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.
There is no separate cache-write charge on Router. When a call writes to a provider’s prompt cache, those tokens are already counted inside
input_tokens, so they arrive in your llm_input_tokens count and your plan’s input rate applies to them. Lago deliberately does not also emit llm_cache_creation_tokens for Router traffic, which would charge the same tokens twice.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
Everything bills as token counts, never priced.provider="ramp_router" is deliberately unmatchable against the price sources, because Router’s effective rate cannot be read from a response: a call served with your own provider key costs $0 through Router, and a non-default service tier bills at a rate Router’s own catalog says may differ from the one it publishes. You set the rate in Lago. Running pricing_mode="price" globally still gets token events here, with no price-miss error, because a structural absence of a rate card is not a lookup failure. This is why we recommend the OpenAI client: it is what lets Lago recognise the call as Router traffic and fall back to token counts instead of pricing it as something else. See Billing for the full reasoning.
Only the OpenAI client is supported. Router also accepts Anthropic’s /v1/messages format, and pointing a wrapped Anthropic client at Router will look like it works, but Lago cannot tell those calls went through Router. It bills them as direct Anthropic usage at Anthropic’s public prices, which may be nothing like what Router charged you. If Router served the call with your own provider key, Router charged you nothing and Lago would still bill full price. Use the OpenAI client shown above.
There is no backfill path. Router’s only routes are GET /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. An analytics API is referenced once in Router’s limits table with no path, no auth and no record shape, which is not enough to build against. 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. Cache reads bill on llm_cached_input_tokens as they would on a direct call, and cache writes bill inside llm_input_tokens at the plain input rate.
A proxy in front of Router is not detected. Reaching Router through your own hostname bills as plain OpenAI, with the wrong provider 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
Why Router bills in tokens, and how to set the rate in Lago.
Configuration reference
Every config knob, in both SDKs.