Skip to content

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.

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 route

There 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.

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.

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.

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.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/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.