Translation
Translation is the second choice, and most of the design is spent not reaching it. This page is about what happens when it does run: which dialect pairs are carried, what each one costs, and why a dialect the gateway could translate is often better left untranslated.
Read fidelity first for how a provider is chosen, and passthrough for why the bytes are left alone when they can be.
When translation runs
Section titled “When translation runs”A request is translated when the provider chosen for it speaks the other dialect. The choice prefers an entry that speaks the request’s dialect, so translation runs only when no such entry claims the model:
entries claiming the model, most specific pattern first the first whose dialect == the request's dialect: passthrough otherwise the first declared: translate to its dialect, or refuse when no wired pair carries the routeThere is no translated endpoint to configure. Translation rides the provider’s own entry, same upstream, same credential, and carries the vendored pair’s losses on top of it. Translation is something this process does, so the losses come from reading our own translator rather than from anyone’s documentation, and they are checked against it on every build.
The two pairs that are wired
Section titled “The two pairs that are wired”| inbound dialect | upstream dialect | what it does not preserve |
|---|---|---|
openai |
anthropic |
audio_input, multi_choice, usage_details |
anthropic |
openai |
strict_tools, prompt_caching, structured_output, usage_details |
Dispatch is decided by the inbound route, not by content: a request on
/v1/messages is an Anthropic-dialect client, one on
/v1/chat/completions is an OpenAI-dialect client. Same-dialect traffic
never enters a translator, including Anthropic to Anthropic, which is
passthrough like any other matched pair.
Each pair carries one route, the chat surface of its inbound dialect.
The other OpenAI routes have no pair, so /v1/responses or
/v1/embeddings in front of an anthropic entry is refused with
route_not_translatable rather than sent as OpenAI bytes to an
Anthropic server. Upstream makes the same refusal when it builds the
translator, as a 500 from the ext_proc call. Here it is a 422 from
admission, and preflight reports it.
A provider can speak the Anthropic dialect from a host whose paths
differ. Claude on Vertex AI is the case: the model moves into the URL
and anthropic_version into the body. That is two edits on the way
out, made after translation where there is one, and not a third
dialect. Adding a provider
has the entry.
The OpenAI to Anthropic pair also requires max_tokens on the way in,
because the Messages API requires it and the translator does not
supply one. Fidelity covers the refusal and why there
is no default.
The losses are real and worth stating plainly. usage_details appears
in both rows. Translated usage collapses the cache and reasoning token
splits, in either direction. A budget typed on one of those splits is
charged by total tokens on a translated path, and the downgrade counter
records each such settlement, so the collapse costs the allowance rather
than the ledger.
Why those two, and nothing else
Section titled “Why those two, and nothing else”The pattern is not “we translated the popular dialects.” Translation is reserved for the gap nobody else can fill.
A dialect requirement can come from either end:
- From the client. Claude Code speaks the Anthropic Messages API. If you want it in front of an OpenAI-compatible backend, something has to translate, and no provider hosts a “Messages API compatibility endpoint” for arbitrary OpenAI upstreams. There is nobody else to do it, so the gateway does it. Both wired pairs are this case.
- From the provider. Here somebody else already did it. Gemini,
Azure and Anthropic all host OpenAI-compatible endpoints. Reaching
them that way is a provider entry with
dialect: openai: byte passthrough from the gateway, with the translation happening in the provider’s own infrastructure, where they own the drift.
When a provider-side option exists, taking it is strictly better. Translation buys a schema to maintain, and the vendor’s endpoint is the vendor’s to keep current.
The Gemini case, worked through
Section titled “The Gemini case, worked through”Gemini is the clearest example, because the temptation is real. Upstream ships a Gemini translator, and vendoring it costs nothing to start.
The recipe takes the other path:
providers: - name: gemini dialect: openai base_url: https://generativelanguage.googleapis.com/v1beta/openai strip_path_prefix: /v1 api_key: ${secret:GEMINI_API_KEY} models: ["gemini-*"]So Gemini traffic is ordinary OpenAI-dialect passthrough. Google translates on its side, and what its endpoint drops is in Google’s documentation.
An in-process Gemini translator would take traffic that is byte passthrough today and make it a parse-and-rebuild, in exchange for Google’s request schema to track forever.
This is the general rule, not a Gemini-specific judgement: a dialect gets translated when no provider-hosted path exists, not when a translator happens to be available.
A dialect that is not carried is cut, not curated
Section titled “A dialect that is not carried is cut, not curated”Upstream ships translators for far more than the two pairs above:
Bedrock Converse, Vertex generateContent, Cohere rerank, embeddings,
speech, token counting. All of it arrives with every sync and none of
it is called. It is removed on the way in, by computation rather than
by a deletion list:
tools/internal/closure.WiredRootsnames the translator entry points the gateway calls, currently eight methods across the two pairs.tools/schemacoveragereports which translator functions are reachable from those roots, and which files have none.tools/vendorsync -pruneremoves the files with nothing reachable, rebuilds, and puts back anything the compiler then finds it needed. Upstream shares helpers across test files, so a file can be dead in production and load-bearing for a test that covers live code. The last sync cut 64 files and, with them, four modules.
A deletion list would be a fork. It goes stale the first time upstream
renames a file, and nothing tells you. The reachability report is the
check, and reviving a dialect is an edit to WiredRoots rather than a
file quietly reappearing.
The cut is also worth making at all, rather than tolerating dead code,
because a translator that is never called still links what it imports.
The Gemini path is the standing example. It keeps
google.golang.org/genai, and with it a GCP credential and mTLS stack,
reachable from a gateway that never authenticates as a Google client.
Pruning the translator files does not by itself remove that, because a
handful of Gemini types are still referenced from the shared OpenAI
schema as vendor fields.
Where the facts in this page live
Section titled “Where the facts in this page live”The losses table is prose. The authority is
internal/fidelity/translator.go, and a test keeps this page honest
against it. That table in turn is not hand-maintained trivia. The
coverage test in internal/fidelity runs one fixture per capability
through the real in-process translators and fails if the curated list
disagrees with what they do.
At request time, POST /v1/preflight says whether a request would be
translated and what the translation would cost it, and
kirto_gate_fidelity_downgrade_total counts what the guard did.