Platform
Router
Meter OpenAI-compatible and Anthropic Messages traffic by changing a base URL instead of code. The router forwards each request with your own provider key and records its tokens and cost.
Availability
The router is for team workspaces. Its provider keys, models and policies are managed by workspace admins under Gateway; see Gateway and routing.The router speaks the OpenAI Chat Completions API at /v1/chat/completions and the Anthropic Messages API at /v1/messages. Point a client at it with your Token Meter ingest key as the API key; it forwards each request with your own provider key (stored encrypted in the dashboard under Gateway › Connect) and meters it. It can also route to cheaper models, cache, and enforce budgets, all configured in the dashboard.
The router's address is on the dashboard's setup page (Connect › Router). The examples below use https://router.example.com.
Before the first request
A workspace admin sets up two things under Gateway › Connect. Without them the router refuses the call.
- An inference key for each provider you call: OpenAI, Anthropic or Google Gemini (or a LiteLLM gateway). Without one the router answers
424 provider_not_configured. - A model capability record for every model you call: its context and output limits and whether it supports tools, structured output and vision. Without one the router answers
400 model_capability_not_configured. A request that sendstoolsto a model saved without Tools gets400 model_does_not_support_tools.
Which provider serves a request
With no routing policy, a model whose name starts with claude- goes to Anthropic and every other model goes to OpenAI. To send traffic to Google Gemini or through a LiteLLM gateway, create a routing policy that targets that model (Gateway › Router). A policy can also move a request to a different model, which is how cheaper-model routing works.
Environment only
The OpenAI SDKs read these, so many apps need no code change:
OPENAI_BASE_URL=https://router.example.com/v1
OPENAI_API_KEY=$TOKEN_METER_KEYIn code
import OpenAI from "openai";
const openai = new OpenAI({ baseURL: "https://router.example.com/v1", apiKey: process.env.TOKEN_METER_KEY });
await openai.chat.completions.create({ model: "gpt-4o-mini", messages: [{ role: "user", content: "Hello" }] });The key can be sent as Authorization: Bearer <your ingest key> or as an x-api-key header.
Anthropic Messages API
Point the Anthropic SDK at the router's origin (the SDK adds /v1/messages itself):
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://router.example.com", api_key=os.environ["TOKEN_METER_KEY"])
client.messages.create(model="claude-haiku-4-5", max_tokens=512, messages=[{"role": "user", "content": "Hello"}])- Requests to
/v1/messagescan only be served by Anthropic models. A routing policy that targets another provider makes them fail with400 native_anthropic_cross_provider_routing_not_enabled; fallbacks and experiments that point at another provider are skipped for them. - Tools,
system,thinking,metadataand thecache_controlmarkers inside messages are passed through. /v1/messagesrequests are never answered from the router's response caches, and are not captured or sampled for shadow model comparisons.
A claude- model called through /v1/chat/completions is translated to Anthropic's format, but only for plain string message content. Tools, tool_choice and response_format are refused there (anthropic_tool_and_structured_output_translation_not_enabled); use /v1/messages for those.
Attribution
Send the tags as top-level fields of the request body. The router records them and removes them before forwarding to the provider.
| Field | Meaning |
|---|---|
action | What the call does, e.g. "generate-report". |
customer_ref | The end customer or tenant (an opaque id). |
feature | The product surface. |
team | The owning team. |
environment | e.g. "prod". |
trace_id | Optional, to group calls. |
// Built as a variable, so TypeScript accepts the extra fields the OpenAI types don't list.
const body = {
model: "gpt-4o-mini",
messages: [{ role: "user" as const, content: "Hello" }],
action: "generate-report",
customer_ref: "acme",
feature: "reports",
};
await openai.chat.completions.create(body);action, feature and the requested model are also what routing policies match on.
Other fields the router reads
These are optional, read by the router and never forwarded to the provider:
| Field | Effect |
|---|---|
max_cost_usd | Refuse the request (402 estimated_cost_exceeds_max_cost_usd) when its worst-case cost is above this amount. |
run_id | Groups calls into one run with a shared budget. Requires max_cost_usd. Up to 200 characters. |
run_budget_usd | The run's total budget. The first call of a run must send it; later values are ignored. Only with run_id. A request that does not fit in what is left gets 402 run_budget_exceeded. |
task_type | A task hint for automatic tiering. How far it is trusted is set under Gateway › Router › Request handling. |
data_classification | Data classes you declare (a string or a list), added to the ones the router detects in the text and checked against governance policies that block data classes. |
sensitive | true keeps the request out of the response caches. |
cache_control | "no-store" keeps the request out of the response caches. |
recall | Agent Recall settings; see the API reference. |
Any other field the router does not know is refused with 400 unsupported_request_field:<name>, so nothing that can raise cost reaches the provider unaccounted for. On /v1/chat/completions the router accepts messages, max_tokens, max_completion_tokens, temperature, top_p, tools, tool_choice, response_format, stop, user, parallel_tool_calls, stream, stream_options, seed, presence_penalty, frequency_penalty, logit_bias, logprobs, top_logprobs, reasoning_effort and prompt_cache_key. n and service_tier are refused.
Streaming works on both endpoints. The router asks the provider for usage on every stream itself, so whatever you send in stream_options is replaced.
Headers
| Header | Effect |
|---|---|
X-TokenMeter-Rollout-Key | A stable key (up to 200 characters) that keeps one user or conversation on the same side of a partial policy rollout or experiment. Without it the router uses customer_ref, then user, then trace_id. |
X-TokenMeter-Idempotency-Key | A UUID. A retry with the same key never makes a second provider call: see below. Any other value is refused with 400 invalid_idempotency_key. |
Prefer: respond-async | Queue the request through the provider's batch API at batch prices instead of answering now. Needs Async at batch rates on under Gateway › Defaults. |
Cache-Control: no-store | Keep this request out of the response caches. |
Reusing an idempotency key:
- Same request, first one completed, and Idempotent replay on (Gateway › Caching): the stored response comes back with
x-tokenmeter-idempotent-replay: trueand no second charge. - Same key, different request:
422 idempotency_key_reused_with_different_request. - First request still running:
409 idempotency_request_in_progresswithRetry-After: 2. - Otherwise:
409 idempotency_key_already_used.
Responses
Every routed response carries x-tokenmeter-routing-decision: <decision id>. Answers served from a cache also carry x-tokenmeter-cache: hit (exact) or semantic-hit with x-tokenmeter-cache-similarity.
GET /v1/routing-decisions/<decision id> (same key) returns that decision's status, selected_provider, selected_model, estimated_max_cost_usd, created_at and completed_at. A streamed response can finish an instant before its decision is marked completed, so poll again briefly if it still reads reserved.
A request with Prefer: respond-async gets 202 with the batch id, poll_url and items_url. Read progress at GET /v1/batches/<id> and the answers at GET /v1/batches/<id>/items. It is refused (400 request_not_eligible_for_batch_arbitrage) when it streams, sends tools or images, uses run_id, run_budget_usd or max_cost_usd, or is served by a provider other than OpenAI or Anthropic.
Errors
Router errors are JSON {"error": "<code>"}. A provider's own error is passed through with the provider's status and body.
| Status | Codes |
|---|---|
| 400 | model_required, invalid_json, unsupported_request_field:<name>, model_capability_not_configured, model_does_not_support_tools (or _structured_output, _vision), max_tokens_exceeds_model_capability, request_exceeds_model_context_capability, prompt_rejected (input guardrail), response_rejected (output withheld, billed: false), tiered_routing_not_configured, model_blocked_by_governance_policy, request_blocked_by_governance_policy |
| 401 | invalid_key |
| 402 | org_max_request_cost_usd_exceeded, policy_max_request_cost_usd_exceeded, estimated_cost_exceeds_max_cost_usd, monthly_budget_exceeded, run_budget_exceeded (each with the estimate and the limit) |
| 403 | batch_arbitrage_not_enabled |
| 424 | provider_not_configured with provider: no inference key for the provider that would serve the request |
| 429 | rate_limit_exceeded with scope org or key |
| 502 | provider_network_failure: the provider could not be reached |
| 503 | provider_circuit_open (with Retry-After: 30), governance_policies_unavailable, monthly_budget_unavailable |
Batches
POST /v1/batches submits up to 1,000 requests to one model as a single provider batch:
{
"model": "gpt-4o-mini",
"action": "nightly-summaries",
"requests": [
{ "id": "item-1", "input": { "messages": [{ "role": "user", "content": "Summarize ..." }], "customer_ref": "acme" } }
]
}provider is optional (openai, anthropic or litellm; Gemini has no batch transport). action and feature at the top level pick the routing policy for the whole batch; attribution fields go in each item's input. Each item's id is your own and must be unique in the batch; items cannot stream or send tools or images. The whole batch is checked against rate limits, cost caps, budgets and governance policies before it is accepted with 202. Poll it with the two GET /v1/batches/... routes above. Send X-TokenMeter-Idempotency-Key to make a retried submission return the batch it already created.
Coding tools: Cline and Continue
Both call models with your own API keys, so the router can meter them without any plugin.
Cline: in Cline's settings, choose the API provider OpenAI Compatible, set the base URL to https://router.example.com/v1, the API key to your Token Meter ingest key, and the model ID to a model your router credentials can reach (for example gpt-4o-mini).
Continue: add a model to config.yaml that points at the router:
models:
- name: GPT-4o mini (metered)
provider: openai
model: gpt-4o-mini
apiBase: https://router.example.com/v1
apiKey: <your Token Meter ingest key>Calls then show on the dashboard with exact tokens and cost. Neither tool sends the attribution fields above, so these calls arrive without a customer, feature or team.
Count each call once
If you also send Cline's OpenTelemetry logs or Continue's data events to Token Meter, the same calls count twice: pick one route per tool.SDK or router, not both
A call sent through the router and also made with a wrapped client is metered twice. Use one per call path.