Token Observe for OpenRouter
The one upstream that reports a real cost, and the one whose routing controls are refused.
On this page
What moves in a OpenRouter client
# On the agent
OPENAI_BASE_URL="https://gateway.example.com/v1" # was https://openrouter.ai/api/v1
OPENAI_API_KEY="acp_agent_…" # the agent key, not the OpenRouter key
# On the gateway, named in the audited provider row
OPENROUTER_API_KEY="…"
# These are refused before egress, not forwarded:
# models, provider, route, plugins, transforms, web_search_options
# and the :online, :nitro, :floor and :exacto model suffixesWhere that traffic lands
- POST /v1/chat/completions
- The ingress, identical to the direct OpenAI one because the adapter is composed from the same client. Three deltas apply on egress: max_tokens rather than max_completion_tokens, no inbound headers forwarded at all, and a usage-accounting request added to every body.
- POST /v1/embeddings
- One of only two provider kinds this is available on. The adapter speaks the OpenAI-compatible POST /embeddings and bearer contract, with the same request governance and usage metering as completions; Anthropic, Google, Azure and Bedrock rows fail closed before credential resolution.
- GET /v1/models
- Registry-filtered to the models the calling agent’s roles permit. A single model fetched by a namespaced id containing a slash — the form OpenRouter uses — is matched by a wildcard route, because one path parameter cannot hold a slash.
What is true of OpenRouter and not of the others
Every provider in this list behaves differently somewhere that matters, and those differences are the reason a single general integration page is not enough.
- usage.cost is authoritative
- OpenRouter is the only upstream fronted here that returns a real USD figure, and when it is present the ledger uses it verbatim rather than pricing token counts against a local table. An aggregator’s blended price cannot be derived from a per-model price table, because which sub-provider served the call decides it.
- A nonsense cost falls back to the price table
- A negative or non-finite figure is discarded rather than written to the ledger. The field is absent rather than zero when unknown, so free stays distinguishable from not reported — a zero would look like a free call and quietly consume no budget.
- The streamed and buffered paths price identically
- The cost is read from the same usage key in a completion body and in a streaming usage chunk, and the streaming usage event carries it onto the accumulated response. Capturing it on the buffered path only was a real defect: the transport a client chose decided whether the ledger matched the invoice.
- Six request fields are refused
- models can select vendor-side fallbacks outside governance; provider can override provider selection and data-policy enforcement; route delegates routing; plugins adds vendor-side processing, egress or charges that cannot be authorised or priced; transforms can alter inspected content; web_search_options enables vendor-side search egress and charges outside tool governance. Each is refused by presence, including a null or empty value, so wire semantics are never silently stripped.
- Four model suffixes are refused
- :online, :nitro, :floor and :exacto are rejected at the resolved-route boundary, because those strings enable the same vendor-side search or provider selection without making it governable. An alias an operator has mapped to a safe base model remains valid.
- No inbound headers are forwarded
- Nothing an inbound client sends is meaningful to this upstream, and the outbound request carries the gateway’s credential, so the forwardable list is empty rather than inheriting OpenAI’s three. Two attribution headers are added instead, and they identify the gateway rather than the agent or the end user.
- Usage accounting is requested anyway
- The body asks for usage accounting explicitly even though it is always returned and the request is a documented no-op, because it is the supported way to ask and an older or self-hosted OpenRouter-compatible endpoint still honours it.
The only upstream whose cost figure is taken at its word
Everywhere else, the ledger prices token counts against your own price rows, matched on provider kind and model. That cannot be right for an aggregator. OpenRouter’s per-request price depends on which sub-provider served the call, and no per-model price table can express a blended figure that changes between two identical requests. So when OpenRouter reports a cost, it is used verbatim.
The reader is deliberately one function applied in two places: to the completion body on the buffered path and to the streaming usage chunk on the streamed path, because OpenRouter nests the figure identically in both. That symmetry is a fix for a real defect rather than a design flourish — the cost was once captured on the buffered path only, which meant the transport a client happened to choose decided whether the spend ledger agreed with the invoice.
The validation around it is small and worth knowing. A negative or non-finite figure is nonsense and is discarded, falling back to the local price table, which is strictly better than writing it into a ledger. And the field is absent rather than zero when it is unknown, because a zero reads as a free call: it would consume no budget, show as free in a report, and be indistinguishable from a genuinely free model.
The rest of the accounting is OpenAI’s convention, because the dialect is OpenAI’s. Cached prompt tokens are reported inside the prompt total and subtracted into their own bucket, giving the same four mutually exclusive buckets every provider produces. Anthropic is the exception among these providers, reporting cache tokens beside the input total rather than inside it, and the two conventions must not be mixed — the error runs between half and nine tenths on cache-heavy traffic.
- Unpriced candidates still fail closed
- The authoritative figure arrives with the response, and a budget decision is taken before egress. So if a USD budget is configured and a candidate on the resolved route has no active price row, the request is refused with ACP_BUDGET_UNPRICED as a 409 rather than being estimated at zero.
- Cumulative counters
- Streaming counters are cumulative and some compatible endpoints emit more than one usage frame, where a later frame can omit or regress a bucket. The merge keeps the greatest validated value per bucket, which never credits a budget and does not mistake a repeated total for an increment.
- Failover changes the pricing basis
- A request that failed over is metered against the provider that served it. If that provider is OpenRouter, the authoritative figure applies; if the chain moved the call to a direct vendor, the local price rows do.
Why OpenRouter’s own routing controls are refused
OpenRouter’s most distinctive feature is that a caller can steer routing from inside the request body. That is exactly the feature a governance layer cannot forward, because every one of those fields moves a decision outside the boundary where model permissions, the agent’s data policy and the price ceiling are applied.
Six fields are refused, each with a stated reason. models can select vendor-side fallback models that the role check never saw. provider can override provider selection and with it the data-policy enforcement that decides where prompts may go. route delegates routing wholesale. plugins adds vendor-side processing, egress or charges that cannot be authorised or priced. transforms can alter the content that was inspected, after it was inspected. And web_search_options enables vendor-side search egress and charges outside tool governance entirely. The refusal is by presence rather than by value — a null or an empty array is still refused — because silently stripping a field changes the meaning of a request that then succeeds.
The same argument extends to the model string. The retained :online, :nitro, :floor and :exacto suffixes enable vendor-side search or provider selection without making either governable, so they are rejected at the resolved-route boundary as well as at ingress. An alias an operator has mapped to a safe base model remains valid, which is the supported way to keep a convenient short name.
What replaces all of it is route rules. A rule carries a model pattern, a primary target and an ordered fallback chain, all of which are configuration an operator wrote and an audit log recorded, and all of which are subject to the agent’s data policy — a fallback the agent’s policy forbids is filtered out of the chain rather than used. That is the trade this provider page exists to state clearly: you give up in-band routing control, and you get routing decisions that appear in the record and that the price ceiling can bind.
Ordinary OpenAI-compatible extras are deliberately not on the refused list. Fields such as seed, response_format and reasoning_effort do not choose a different provider or model and do not attach an unpriced vendor-side processor, so they arrive as passthrough — governed like any other text, with routing removing any failover target that would drop or reinterpret them.
What can and cannot be asserted about an aggregator
Every provider row carries three independent data-policy assertions — zero retention, no training on payloads, and a serving region — and they are three rather than one because providers genuinely differ on each: a provider may retain but not train, or train but not retain, and region pinning is orthogonal to both. Routing honours all three separately, and an agent whose record requires one of them will not be routed to a provider whose row does not assert it.
For OpenRouter the shipped template asserts no no-training claim, and the reason is stated in the source rather than left to inference: OpenRouter fans out to many upstreams under their own terms, so no such claim can honestly be made on its behalf. That is a limitation of what is knowable, not a gap waiting to be filled by a checkbox. If you need a no-training assertion for a class of traffic, route that traffic to a direct provider row where the assertion corresponds to an agreement you hold.
It is worth being precise about what these booleans are in general. They are operator declarations about a contract, recorded and audited on the row, and they are not verified against the agreement they describe. Token Observe enforces them as routing constraints — an agent requiring a region is not routed to a provider row that does not declare it — and it does not and cannot check that the vendor honours what you asserted.
The routing consequence is worth planning for. If an agent’s data policy cannot be satisfied by any enabled provider that can serve the model, the request is refused before egress with a typed error naming the constraint rather than being served by a provider that does not meet it. That is the correct failure, and it is the reason to think about which traffic goes through an aggregator before, rather than after, the first refusal.
- Attribution headers
- Two headers are added to every outbound call and show on the account’s activity page. They identify the gateway, never the agent and never the end user, which is the same rule that keeps agent identifiers out of third-party telemetry elsewhere.
- Priority in the shipped template
- Direct vendors come first and the aggregator last, on the reasoning that a request a first-party API can serve should not be brokered. Priority is a provider-row field, so that ordering is yours to change.
- The price catalogue
- The model price catalogue Token Observe can sync from is hosted at openrouter.ai, and that outbound call is on the closed list of destinations this process opens connections to. Syncing prices is separate from routing traffic through the aggregator.
Failover through an aggregator, which is a narrower question than it looks
OpenRouter does its own fallback internally, and that is precisely the behaviour the refused fields turn off. What remains is the gateway’s own chain, decided by failure class rather than by a retry count: timeout, rate_limited and server_error walk down the route rule’s fallbacks, while context_too_long, content_policy, auth and invalid_request do not.
The content-policy case deserves attention on an aggregator specifically. A refusal that reaches you through a broker is still a refusal by whichever safety system produced it, and replaying it at another vendor is a second attempt at the same action with the objection hidden behind a recorded success. Classification is a lossy mapping from heterogeneous vendor error shapes onto seven classes, and a broker adds a layer to that mapping — the published residual is that a provider returning 5xx for what is really a refusal will be failed over, producing exactly the laundering the design prevents elsewhere.
The auth case is simpler and is worth checking first when calls start failing: an authentication failure means the gateway’s OpenRouter key is wrong, revoked or unfunded, and it fails identically wherever that credential is used, so it never fails over. Failing over would mask a broken key behind a more expensive provider until the invoice arrives.
Above the chain, five consecutive failures opens this provider’s circuit breaker for thirty seconds. The breaker survives configuration refreshes, so editing a provider row does not hand a flapping upstream a clean slate, and the metrics gauge reads the same breaker the request path consults rather than a second instance that would show healthy during an outage.
The parts of the platform this uses
Model routing
Six upstreams behind one set of policies, and a fallback chain that will not launder a refusal.
Spend controls
Hard USD ceilings, per-minute rate limits and a kill switch, all decided before the request leaves your network.
Policy engine
One deterministic verdict on every governed request: allow, block, redact, or park it for a human.
Flight recorder
Every governed request in a timeline a compliance officer can read, and a search box that never writes SQL.
The same policies apply identically whichever provider serves the request, and that equivalence is enforced by a test over every provider kind rather than asserted.
See the request pathThe rest of the upstreams
Why are OpenRouter’s provider and models fields rejected?
Because each one moves a decision outside the boundary where model permissions, the agent’s data policy and the price ceiling are applied. models can select vendor-side fallback models the role check never saw; provider overrides provider selection and with it data-policy enforcement; route delegates routing wholesale; plugins adds vendor-side processing, egress or charges that cannot be authorised or priced; transforms can alter content after it was inspected; and web_search_options enables vendor-side search egress outside tool governance. The refusal is by presence, including a null or empty value, because silently stripping a field changes the meaning of a request that then succeeds. Configure fallbacks and provider constraints on route rules instead, where an operator wrote them and the audit log recorded them.
Does the cost figure come from OpenRouter or from my price table?
From OpenRouter, when it reports one. It is the only upstream fronted here that returns an authoritative USD figure inline, and an aggregator’s blended price cannot be derived from a per-model price table because which sub-provider served the call decides it. The same reader is applied to the completion body and to the streaming usage chunk, so the transport your client chose does not decide whether the ledger agrees with the invoice — capturing it on the buffered path only was a real defect. A negative or non-finite figure is discarded in favour of the local price table, and the field is absent rather than zero when unknown, so a free model stays distinguishable from an unreported cost.
Can I claim no-training for traffic routed through OpenRouter?
Not honestly, and the shipped provider template does not. OpenRouter fans out to many upstreams under their own terms, so no no-training claim can be made on its behalf. The three data-policy assertions on a provider row — zero retention, no training, and a serving region — are independent because providers differ on each, and they are operator declarations about a contract, recorded and audited here rather than verified against the agreement they describe. If a class of traffic needs a no-training assertion, route it to a direct provider row where that assertion corresponds to an agreement you hold. An agent whose record requires an assertion no enabled provider makes is refused before egress with a typed error naming the constraint, which is the correct failure.
What happens to a model name with an :online or :nitro suffix?
It is refused, at ingress and again at the resolved-route boundary. Those suffixes — along with :floor and :exacto — enable vendor-side search or provider selection through the model string rather than through a body field, which is the same delegation the refused fields represent, wearing different clothes. An alias an operator has mapped to a safe base model stays valid, so a convenient short name is still available; what is not available is a caller changing where and how a request is served by editing the model string. This is deliberately a provider-family contract rather than a promise that an arbitrary vendor extension is portable across unrelated APIs.
Should I put an aggregator first or last in my routing?
The shipped template puts direct vendors first and the aggregator last, on the reasoning that a request a first-party API can serve should not be brokered — and priority is a field on the provider row, so that ordering is yours to change. Two facts should inform the decision. Cost is more accurate through OpenRouter, because the vendor reports the real figure and the ledger uses it verbatim rather than approximating a blended price. And data policy is weaker, because no no-training claim can honestly be made on the aggregator’s behalf. A common shape is direct rows for traffic with a data-policy requirement and the aggregator as a fallback for capacity, expressed as a route rule so the decision appears in the record.
Name the SDK and the version.
A base-URL change is the normal case for the OpenRouter dialect, and whether your own client library and version behave that way is the first thing worth checking. Say which you use and you will get a straight answer.
no form · no qualification step · no sales desk · the other three ways in