Translation
Translation is the third 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 channels and fidelity first for the tier ladder, and passthrough for why the bytes are left alone when they can be.
Translation is not a tier
Section titled “Translation is not a tier”native and provider_compat are tiers a channel can declare.
Translation is not:
Translation by the vendored translators is not a declared tier: it is what resolution falls back to when no channel matches the inbound dialect.
That is not a naming quibble. A tier is something a provider’s catalog entry claims about an endpoint, dated and attributable. Translation is something this process does, so there is no external claim to cite and no endpoint to point at. The losses come from reading our own translator, not from someone’s documentation.
Resolution reflects that ordering directly. It collects candidates by
walking native then provider_compat for channels matching the
inbound dialect and the model gate, and only then considers translation:
for tier in [native, provider_compat]: channels where dialect == inbound and model matchesthen: a native channel where dialect != inbound, model matches, and a translator pair is wired for (inbound -> its dialect)The last step is worth reading closely, because translation is not a separate endpoint. It rides the native channel, same upstream, same model gate, and carries the vendored pair’s losses on top of it. There is no “translated channel” to configure, and a provider with no native channel gets no translated one either.
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 at all, including Anthropic to Anthropic,
which is passthrough like any other matched pair.
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. That is why require_caps: [usage_details]
on a budget-metered profile pins it off translated paths. A profile’s
requirement is policy, and no on_fidelity_loss: allow overrides it.
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
provider_compat: still 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 moves a request down the ladder, and buys a schema to maintain.
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 catalog entry takes the other path:
"schema": {"name": "OpenAI"},"base_url": "https://generativelanguage.googleapis.com/v1beta/openai","channels": [{"id": "openai-compat", "dialect": "openai", "tier": "provider_compat", "caps": ["tools", "strict_tools", "structured_output", "thinking", "vision", "audio_input", "multi_choice"]}]So Gemini traffic is ordinary OpenAI-dialect passthrough. Seven of the
nine capabilities survive, and the two that do not, prompt_caching
and usage_details, would not have survived translation either, since
translated usage drops the detail splits in both directions.
So an in-process Gemini translator would take traffic that is byte passthrough today and make it a parse-and-rebuild, in exchange for strictly less fidelity, plus 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/catalog/translatorcaps.go, and a test keeps this page honest
against it. That table in turn is not hand-maintained trivia. The
coverage report in internal/catalog runs fixtures through the real
in-process translators and fails if the curated list disagrees with
what they do.
At runtime, GET /admin/v1/catalog/report shows every routable channel
with its tier and provenance, and each routing decision records the
claim it acted on as pistra.channel_origin.