Headers reference

Request headers

HeaderRequiredNotes
Authorization: Bearer sk_…yesYour odnoga API key. sk_live_ or sk_test_ — both behave the same and both bill; the prefix only labels the environment.
Content-Type: application/jsonyesAll endpoints are JSON.
x-odnoga-end-userstrongly recommendedStable per-user id, ≤256 chars. Powers metering, quotas, A/B stickiness. The pre-rebrand x-airouter-end-user is still honoured, as is the OpenAI-compatible user body field.
Idempotency-Key (also x-odnoga-idempotency-key, x-airouter-idempotency-key)optionalNon-streamed requests only: a retry with the same key and body replays the stored response instead of calling the vendor again (x-airouter-idempotent-replay: 1, not billed twice). Same key while the first call runs → 409 idempotency_in_progress; same key with a different body → 422 idempotency_key_reused. Ignored when "stream": true. See Retries and timeouts.
x-airouter-timeout-msoptionalYour deadline for the model, in milliseconds. odnoga waits at most 300 s by default; a smaller value (clamped to 1 000–300 000) makes a slow model fail over to the next one in your chain sooner. It can only shorten the deadline, never extend it.
x-airouter-regionoptionalProcessing region (global / eu / us). Overrides the workspace default for this call.
X-Odnoga-Appstrongly recommendedWhich system is calling, e.g. salesdots. Slugified, ≤64 chars.
X-Odnoga-Functionstrongly recommendedWhich function/job inside it, e.g. lead-enrich. Slugified, ≤64 chars.
X-Odnoga-Envoptionalproduction, staging, …
x-odnoga-streamoptionalfinal on a streamed /v1/chat/completions call: the connection opens at once and is kept alive with : keepalive comments, but you receive one frame — the complete chat.completion with usage and _odnoga cost — then [DONE]. For callers that stream only to escape a ~150 s platform limit. See Streaming → Final-only mode.

Identify your integrations

Label every call with the app and function that made it, and the workspace Functions view turns into a per-function line item: request volume, success rate, p50/p95 latency, tokens, cost, models used, and the exact error codes when something breaks — so you see which function in your system is failing, not just "a request failed".

curl https://api.odnoga.com/v1/chat/completions \
  -H "Authorization: Bearer sk_live_…" \
  -H "X-Odnoga-App: salesdots" \
  -H "X-Odnoga-Function: lead-enrich" \
  -H "Content-Type: application/json" \
  -d '{"model":"gpt-4o-mini","messages":[{"role":"user","content":"hi"}]}'

If your client cannot set headers (some SDKs and no-code tools), send the same values in the request body instead — they are stripped before the call reaches the vendor:

{ "model": "gpt-4o-mini", "messages": [], "metadata": { "app": "salesdots", "function": "lead-enrich" } }

Both headers are read on every endpoint — /v1/chat/completions, /v1/embeddings, /v1/messages, the native endpoint, and the image and audio paths. (Before 2026-09-16 only the OpenAI-compatible chat path read them, so traffic through the others was unattributable however carefully the client was configured.)

Unlabelled traffic is still visible — it is grouped by API key label, shown as unattributed. Inside odnoga you can give any function a friendly display name, an owner/description and tags; the slug stays the key, so renaming never breaks history.

Check how much of your spend has an owner

The Functions page leads with a coverage bar, and the MCP tool functions.list returns the same figures as attribution:

{ "requests": 60059, "attributed_pct": 79.9,
  "cost_usd": 71.85, "unattributed_cost_usd": 32.46, "unattributed_cost_pct": 45.2,
  "top_unattributed": [ { "model_slug": "gpt-5.6-terra", "endpoint": "/v1/chat/completions",
                          "requests": 6383, "cost_usd": 23.11 } ] }

One trap worth knowing: unlabelled traffic still shows up in the Functions list, grouped under the API key's label with the app shown as unattributed. That row reads like a function someone named, but nothing set it — which is why a coverage figure saying "$32 has no caller" can sit directly above a list showing a "function" that spent $32. They are the same fact; grouped_under_key_label names those rows so the two do not look like a contradiction.

Read the cost share, not the request share. They diverge, and only one of them is the bill: a thousand unlabelled embedding calls are noise, ten unlabelled frontier calls are most of the invoice. top_unattributed gives the fix an address — the models and endpoints the missing labels are arriving on.

Response headers (success)

HeaderMeaning
x-airouter-request-idUUID of this request in odnoga. Log it — it is what Requests and requests.search look up.
x-airouter-vendorVendor that actually served the call (may differ from requested via fallback).
x-airouter-modelModel slug that actually served the call.
x-airouter-response-formatopenai or native — the shape of the body you just received (a workspace setting; see below). Streams are always openai.
x-airouter-prompt-tokensPrompt tokens billed for this request. 0 on cache hits.
x-airouter-completion-tokensCompletion tokens billed. 0 on cache hits.
x-airouter-total-tokensTotal tokens billed. 0 on cache hits.
x-airouter-cost-usdWhat this single request is charged, in USD — the model's price plus odnoga's fees (on your own vendor key, odnoga's fees only). The same amount the request row and your balance show. 0 on cache hits.
x-airouter-cost-basisbilled when x-airouter-cost-usd is the charged amount (normal). list in the rare case the charge could not be confirmed in time and the header shows the model's price only — the request row in Requests always holds the charged amount.
x-airouter-service-tierThe tier this request was billed at (standard, priority, …) — never the one it merely asked for.
x-airouter-tier-downgraded1 when the vendor could not honour the requested tier and the call was billed as standard. Absent otherwise.
x-airouter-regionProcessing region the request was priced under.
x-airouter-region-honored1 only when the call actually left through a regional vendor endpoint.
x-airouter-latency-msEnd-to-end latency measured at the gateway.
x-airouter-cachehit / miss / off.
x-airouter-fallback1 if a fallback model was used, 0 otherwise.
x-odnoga-route / x-odnoga-route-version / x-odnoga-route-chainThe route that served the call, its version, and the models it was going to try, in order. Only when a route served it.
x-odnoga-route-connected<app>/<function> when the route served the call because that function is connected to it — whatever model the code sent.
x-ratelimit-remaining-usdRemaining USD: the lower of your prepaid credit and the active budget window. Present when either applies.
x-ratelimit-resetUnix seconds when the active budget window rolls over. Only present when your workspace or organisation has a budget window; it is not a retry hint.
x-ratelimit-limit-concurrency / x-ratelimit-remaining-concurrencyWorkspace concurrency ceiling and how much of it is left right now. Size your worker pool on these instead of discovering the limit through 429s.
x-ratelimit-limit-concurrency-tenant / x-ratelimit-remaining-concurrency-tenantThe same for the organisation-wide session pool shared by all your workspaces.
x-ratelimit-limit-requests / x-ratelimit-remaining-requestsRequests-per-minute limit and what is left this minute. Only when an RPM limit is set for the workspace.
x-ratelimit-limit-tokens / x-ratelimit-remaining-tokensTokens-per-minute limit and what is left this minute. Only when a TPM limit is set for the workspace.
x-airouter-skipped-modelsModels in your chain that were passed over before one served, as comma-separated model=reason (e.g. gpt-4o=cooldown:openai, o3=missing:vision, old-model=retired). Also sent on errors.
x-airouter-key-sourcepooled when odnoga's credential served the call, own when your own vendor key did.
x-airouter-idempotent-replay1 when this response is a stored replay for a repeated idempotency key.
x-odnoga-pool-exceeded / x-odnoga-pool-exceeded-scopePresent only when the call was served past your concurrent-session ceiling. 1 plus tenant or workspace. Nothing failed and nothing extra was charged — see Concurrent sessions.
WarningAccompanies the header above with a readable sentence naming the ceiling that was exceeded.
x-odnoga-streamOn streams: final when final-only mode was honoured, tokens for a normal token stream.
x-airouter-strippedParameters odnoga removed because this model does not accept them (comma-separated), e.g. temperature on a reasoning model. The call still ran.
x-airouter-clampedParameters moved into the model's accepted range, as name=from->to.
x-odnoga-dialectWhat odnoga changed inside your JSON schemas so the vendor that answered accepts them, as surface:keyword=action(count) — e.g. tool_schema:$ref=inline(2) when that vendor does not take $ref in tool parameters, so the definitions were written out in place. surface is tool_schema (tool parameters) or response_schema (a strict json_schema answer — a non-strict one is always sent as written); action is drop, inline, or rewrite (oneOf sent as anyOf, allOf merged into its schema — for a vendor that refuses them in strict mode). Which keywords each vendor accepts is measured weekly, never guessed; a keyword the vendor accepts is always sent as you wrote it. Absent when nothing changed.
x-airouter-setParameters odnoga set because the model requires them, as name=value — e.g. reasoning_effort=none on a model that accepts tools only at that effort.
x-airouter-renamedParameters renamed to the model's own name for them, e.g. max_tokens->max_completion_tokens.

Model lifecycle headers

Vendors retire models. odnoga announces a retirement ahead of time and never swaps a model silently.

HeaderWhenMeaning
Deprecation: @<unix-seconds>the model has an announced retirement dateRFC 9745. The model still serves; move off it before the date.
Sunset: <HTTP-date>sameRFC 8594. The date the slug stops serving.
x-odnoga-model-sunsetsameThe same date as ISO-8601.
x-odnoga-model-replacementsame, when a replacement is namedThe slug to move to.
x-odnoga-model-fallback: <retired>-><served>the request named a retired model and the organisation enabled automatic fallbackThe same-vendor replacement served this call. x-airouter-model is the model that actually served.
x-odnoga-model-retiredsameThe retired slug the request asked for.

The _odnoga body echo carries the same facts as model_sunset_at, model_replacement and model_fallback: { retired, served }.

Automatic fallback is off by default. With it off — which is where every organisation starts — a retired model returns 400 model_deprecated and nothing is substituted (see the errors reference). Turn it on in Organisation → Defaults → Retired models, or over MCP:

settings.model_lifecycle.get    → the switch, plus the retired and sunsetting models YOU called in the last 30 days,
                                  which of your functions called them, and whether each replacement can actually serve
settings.model_lifecycle.set    → { "retired_model_fallback": true }   (organisation-wide; organisation admins only)

It is a stopgap, not a migration: the response still names the retired slug in x-odnoga-model-retired so the caller can be fixed. And it cannot save a model whose replacement is itself retired, or that has none named — check replacement_live before relying on it. Organisation admins are emailed and shown an in-app notice when a retirement is announced for a model they call, a week before the date, on the day, and once a day while an integration still sends the old name.

Token headers are the billing record

Read tokens and cost from the headers, never by parsing the response body. The body's shape is a workspace setting now (x-airouter-response-format: openai or native), and in native mode every vendor spells usage differently — Gemini usageMetadata.promptTokenCount, Claude usage.input_tokens — so a body parser breaks the day the setting flips. The x-airouter-*-tokens headers carry the same numbers odnoga bills from, whatever the body looks like. The body's _odnoga block (attached in both modes) additionally carries request_id, cost_usd (the charged amount, more decimal places than the header) with cost_basis, the catalog model slug, vendor_model provenance, tier, region and any warnings — it is the assertable per-call record for tests.

End-user (EUT) responses are redacted

Responses served to an end-user token (eut_) never include your cost basis or vendor identity: x-airouter-cost-usd, x-airouter-cost-basis, x-airouter-vendor and x-airouter-region-endpoint are removed, and cost_usd / cost_basis / vendor / vendor_model are stripped from the _odnoga block, on every response path. End users keep request_id, the catalog model slug, tokens, region and tier flags.

Response headers (error)

Errors still carry x-airouter-request-id so you can look the row up in Requests. Budget errors also carry x-ratelimit-remaining-usd and, when a budget window exists, x-ratelimit-reset. Rate-limit and capacity answers (429, 503 capacity_unavailable, 409 idempotency_in_progress) carry Retry-After (seconds) — honour it; the official SDKs already do. When every model in the chain was passed over, x-airouter-skipped-models says why and x-airouter-cooldown-until (unix seconds) says when the first one is expected back. A 503 vendor_unavailable or 503 billing_unavailable carries x-odnoga-ticket.

All of these headers are exposed to browsers (Access-Control-Expose-Headers), so a front-end can read them too.

What to log on the consumer side

At minimum, in every request log line:

  • x-airouter-request-id
  • x-airouter-vendor, x-airouter-model
  • x-airouter-cost-usd, x-airouter-total-tokens, x-airouter-latency-ms
  • x-airouter-cache, x-airouter-fallback
  • x-airouter-response-format (so a shape change is visible in your own logs)

Then a single request_id cross-references your logs with the odnoga request log.

Reading the same data over MCP

Everything the Functions and Requests pages show is available to MCP clients (Claude, Cursor, Lovable) on the same identity headers:

ToolScopeWhat it returns
functions.listusage:readEvery external function, with requests, errors, success rate, p50/p95 latency, tokens, cost, models used, last seen. Filter by tag, search, include_archived.
functions.getusage:readOne function in detail: error breakdown, model split, latency, recent request sample. Keyed by app + function.
functions.updateusage:writeRename, describe, tag or archive a function. Only the fields you pass change.
requests.searchusage:readIndividual requests filtered by app, function, model, status (ok/error) and free text; paginated with limit/offset, each row carries total_count.

usage:write is a mutating scope, so clients must be re-consented (remove and re-add the odnoga MCP server) before functions.update becomes available.

Spend caps over MCP

There is no MCP tool that writes a workspace budget. An assistant that is blocked by a spend cap must not be able to lift its own cap.

ToolScopeEffect
account.limitsbudgets:readReads credit, cap, period, warn threshold, hard_stop, blocked.
account.budget.request_changebudgets:writeFiles a proposal (cap_usd / period / warn_at_pct / hard_stop + reason). Nothing changes.
account.budget.change_statusbudgets:readPolls for approved / rejected / expired.

Proposals appear in Workspace → Budget → Pending approvals and only take effect when a workspace admin approves them. They expire after 24 hours.