Fidelity
A request is passed through byte for byte when its provider speaks the dialect it arrived in, and translated when the provider does not. This page is about how the provider is chosen, when translation happens, and what the fidelity guard does about a translation that would drop something.
A provider speaks one dialect
Section titled “A provider speaks one dialect”A provider entry is a name, a dialect and a URL. dialect is openai
or anthropic, and it says what the endpoint accepts: a server that
serves /v1/chat/completions is openai, one that serves
/v1/messages is anthropic. There is no third value. A dialect ships
with a translator, a probe and a coverage fixture.
Vendors host each other’s dialects. Anthropic serves its Messages API and an OpenAI-compatible endpoint. Bedrock serves both, split by model family. Gemini serves only an OpenAI-compatible endpoint. Each of those surfaces is one provider entry in its own dialect, and a vendor with two surfaces is two entries claiming the same models:
providers: - name: anthropic dialect: anthropic base_url: https://api.anthropic.com api_key: ${secret:ANTHROPIC_API_KEY} models: ["claude-*"] - name: anthropic-openai dialect: openai base_url: https://api.anthropic.com api_key: ${secret:ANTHROPIC_API_KEY} models: ["claude-*"]Two entries may claim one model only when their dialects differ. Two claiming it in the same dialect are refused at load, because file order would decide between them and nobody wrote that on purpose.
The request’s dialect picks the entry
Section titled “The request’s dialect picks the entry”The route a request arrives on fixes its dialect: /v1/messages is an
Anthropic-dialect client, /v1/chat/completions and /v1/responses
are OpenAI-dialect clients. Among the entries claiming the requested
model, the first that speaks the request’s dialect is chosen, and the
request is passed through. When none does, the first declared is
chosen and the request is translated to it.
Two routes are translated: /v1/chat/completions to an anthropic
entry, and /v1/messages to an openai entry. /v1/responses,
/v1/completions, /v1/embeddings and /v1/realtime exist only in
the OpenAI dialect, and no translator carries them. A request on one of
them whose chosen entry speaks anthropic is refused with 422
route_not_translatable. The refusal comes before the budget hold and
before anything is sent. A failover candidate in that position is
skipped. /v1/models is served in both dialects, and a path the
gateway has no label for, such as /v1/messages/count_tokens, cannot
be placed in either. Both are passed through to the chosen entry.
With the two entries above, a Claude Code session on /v1/messages
reaches anthropic untouched, and an OpenAI SDK on
/v1/chat/completions reaches anthropic-openai untouched. Anthropic
does the translation in the second case, on its own infrastructure,
and what its endpoint drops is Anthropic’s to document. Declare only
anthropic and the OpenAI SDK’s request is translated by the gateway
instead, under the guard below.
The choice is per request. A model matching one entry’s exact name and
another’s * prefix goes to the exact match; among prefixes the
longest wins; an alias ranks below every pattern; the default provider
catches the rest. The dialect preference applies within whichever of
those groups matched.
The guard
Section titled “The guard”Translation drops things. Translation lists which
capabilities each of the two pairs does not preserve, and the list is
checked against the vendored translator on every build. Before a
translated request is forwarded, the gateway reads which of those
capabilities the body uses. That read is a bytes.Contains prefilter,
then a gjson structural confirm, memoized, and never run on a
same-dialect request because there is nothing to decide.
A request that uses a capability its translation would drop is decided
by on_fidelity_loss, on the deployment or on the provider:
reject(default) refuses with a structured error naming every such capability, so the request is fixed in one pass instead of one retry per loss.allowforwards anyway and counts the downgrade inkirto_gate_fidelity_downgrade_total{provider,cap,action}.
usage_details is the one capability no probe can read, because it is
a property of the response: whether the usage block keeps the cached
and reasoning token splits. Translated usage carries no splits in
either direction, so a budget typed on a split is charged by total
tokens on a translated path, counted under the same metric with the
action metered_by_total.
A translated request can also leave out a field the provider’s dialect
requires. max_tokens is optional in the OpenAI dialect and required
by the Anthropic one, and the translator sends it as 0 when it is
absent. The gateway refuses such a request with 400
required_field_missing and names the field and both of its OpenAI
spellings, max_tokens and max_completion_tokens. The refusal comes
after the guard, before the budget hold, and a failover candidate in
that position is skipped. The gateway does not fill in a default. An
absent max_tokens means no cap below the model’s own, and any number
the gateway chose would cut long answers short with only
finish_reason: "length" to say so.
POST /v1/preflight answers the whole question without sending
anything: which provider, whether the request would be translated,
what the body uses and what the translation would lose.
What the gateway claims, and what it does not
Section titled “What the gateway claims, and what it does not”Every fidelity claim here is about code this process runs. A
translator’s losses are a table in internal/fidelity, and a fixture
per capability runs through the real translator to hold the table to
what the code does.
The gateway makes no claim about another party’s server. A vendor’s compatibility endpoint is a provider entry in its own dialect, passed through like any other, and what that endpoint does with the bytes is in the vendor’s documentation, not in a table here. The recipe for such a surface says so in its note.
Related
Section titled “Related”- Translation: the two pairs, what each drops, and why an available translator is not a reason to use one.
- Passthrough: what byte for byte means when the dialects match.
- Provider recipes: both surfaces written out for the vendors that offer both.