Dashboard →

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.

  1. 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.
  2. 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 sends tools to a model saved without Tools gets 400 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:

Shell
OPENAI_BASE_URL=https://router.example.com/v1
OPENAI_API_KEY=$TOKEN_METER_KEY

In code

TypeScript
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):

Python
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/messages can only be served by Anthropic models. A routing policy that targets another provider makes them fail with 400 native_anthropic_cross_provider_routing_not_enabled; fallbacks and experiments that point at another provider are skipped for them.
  • Tools, system, thinking, metadata and the cache_control markers inside messages are passed through.
  • /v1/messages requests 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.

FieldMeaning
actionWhat the call does, e.g. "generate-report".
customer_refThe end customer or tenant (an opaque id).
featureThe product surface.
teamThe owning team.
environmente.g. "prod".
trace_idOptional, to group calls.
TypeScript
// 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:

FieldEffect
max_cost_usdRefuse the request (402 estimated_cost_exceeds_max_cost_usd) when its worst-case cost is above this amount.
run_idGroups calls into one run with a shared budget. Requires max_cost_usd. Up to 200 characters.
run_budget_usdThe 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_typeA task hint for automatic tiering. How far it is trusted is set under Gateway › Router › Request handling.
data_classificationData 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.
sensitivetrue keeps the request out of the response caches.
cache_control"no-store" keeps the request out of the response caches.
recallAgent 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

HeaderEffect
X-TokenMeter-Rollout-KeyA 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-KeyA 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-asyncQueue 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-storeKeep 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: true and no second charge.
  • Same key, different request: 422 idempotency_key_reused_with_different_request.
  • First request still running: 409 idempotency_request_in_progress with Retry-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.

StatusCodes
400model_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
401invalid_key
402org_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)
403batch_arbitrage_not_enabled
424provider_not_configured with provider: no inference key for the provider that would serve the request
429rate_limit_exceeded with scope org or key
502provider_network_failure: the provider could not be reached
503provider_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:

JSON
{
  "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:

config.yaml
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.