Skip to content

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.

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 matches
then:
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.

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.

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.

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.WiredRoots names the translator entry points the gateway calls, currently eight methods across the two pairs.
  • tools/schemacoverage reports which translator functions are reachable from those roots, and which files have none.
  • tools/vendorsync -prune removes 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.

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.