Which client formats reach which upstreams
Every client-format and upstream pair, streaming and not, plus the two dialects TokenTriage drives outward but does not accept inward.
On this page
You already have an SDK wired into your application, and the model you want next sits behind a different vendor’s API. Before you change anything, you want to know whether the client you have can reach that model, and whether the answer changes once you turn streaming on.
This page is the pair-by-pair answer. Two client request formats can be translated out of:
OpenAI-format and Anthropic-format. Four upstream dialects can be translated into: openai,
anthropic, gemini and bedrock. Every pair is listed twice, once for a buffered response and
once for a stream, which makes sixteen cells.
Find your client format, read across to the upstream you want, then take the mode that matches your call. Where a pair translates, this page names the test that drives it.
This page states what reaches what. It does not turn anything on. The config that enables a translated route is on Cross-dialect translation, what a crossing costs you is on What a translation drops, and how you find out, and what a translated stream does when a frame fails halfway through is on Translated streams.
The grid
Most readers need one cell of this table and none of the rest of the page. Rows are the format your client speaks; columns are the dialect the upstream speaks.
| Client format | Mode | → openai |
→ anthropic |
→ gemini |
→ bedrock |
|---|---|---|---|---|---|
| OpenAI-format | non-streaming | native | translated | translated | translated |
| OpenAI-format | streaming | native | translated | translated | translated |
| Anthropic-format | non-streaming | translated | native | translated | translated |
| Anthropic-format | streaming | translated | native | translated | translated |
| Gemini-format | either mode | not accepted inbound | not accepted inbound | native | not accepted inbound |
| Bedrock-format | either mode | not accepted inbound | not accepted inbound | not accepted inbound | native |
The last two rows are the grid’s labelled blanks rather than missing work. gemini and bedrock
ship as upstream dialects and not as client formats, which is a product decision recorded in code;
Gemini-format and Bedrock-format clients has what
happens to a request that arrives in one of them.
Three verdicts appear in that table, and they are three different paths through the proxy.
| Cell | What happens to the request | What the ledger row carries |
|---|---|---|
native |
relayed on its own dialect, byte for byte; nothing is rendered into another shape | no disclosures.translation block |
translated |
read into the canonical form, rendered into the upstream’s dialect, and the reply rendered back into the client’s | disclosures.translation with from, to, and anything that could not cross |
not accepted inbound |
not translated; the route falls back to the same-dialect upstream and relays byte for byte | decision.fail_safe: true, and no disclosures.translation block |
The same sixteen cells are drawn below.
The figure carries the same claim as the table: two client formats reach four upstream dialects in both stream modes, and the two dialects with no filled row are translation targets rather than client formats.
The twelve translated cells, and the test behind each
A cell that says a pair translates is a claim, so each one carries an end-to-end test that drives
it. The test sends a client request in that format through the proxy at a mock upstream shaped like
the target dialect, and checks the exchange end to end. All twelve pass on a local run against the
product tree at commit 3693f7bd, dated 2026-08-24.
The mocks set the limit of what a cell claims. They are shaped like each provider’s wire, and they are not the providers’ live endpoints, so a filled cell says the translation works against that wire and says nothing about whether a vendor’s production endpoint behaved the same way that morning.
| Client format | Upstream | Mode | A test you can run |
|---|---|---|---|
| OpenAI-format | anthropic |
non-streaming | TestCrossDialectTranslatedRow (test/e2e/translate_test.go) |
| OpenAI-format | anthropic |
streaming | TestCrossDialectStreamTranslatedRow (test/e2e/translate_stream_test.go) |
| OpenAI-format | gemini |
non-streaming | TestCrossDialectOpenAIClientGeminiUpstream (test/e2e/translate_targets_test.go) |
| OpenAI-format | gemini |
streaming | TestCrossDialectStreamOpenAIClientFromGemini (test/e2e/translate_stream_pairs_test.go) |
| OpenAI-format | bedrock |
non-streaming | TestCrossDialectOpenAIClientBedrockUpstream (test/e2e/translate_targets_test.go) |
| OpenAI-format | bedrock |
streaming | TestCrossDialectStreamOpenAIClientFromBedrock (test/e2e/translate_stream_pairs_test.go) |
| Anthropic-format | openai |
non-streaming | TestCrossDialectAnthropicInboundResponse (test/e2e/translate_response_test.go) |
| Anthropic-format | openai |
streaming | TestAnthropicInboundStreamFromOpenAI (test/e2e/translate_stream_pairs_test.go) |
| Anthropic-format | gemini |
non-streaming | TestCrossDialectAnthropicClientPathBorneUpstreams/gemini (test/e2e/translate_targets_test.go) |
| Anthropic-format | gemini |
streaming | TestAnthropicInboundStreamFromGemini (test/e2e/translate_stream_pairs_test.go) |
| Anthropic-format | bedrock |
non-streaming | TestCrossDialectAnthropicClientPathBorneUpstreams/bedrock (test/e2e/translate_targets_test.go) |
| Anthropic-format | bedrock |
streaming | TestAnthropicInboundStreamFromBedrock (test/e2e/translate_stream_pairs_test.go) |
From a checkout of the product, one of them runs as go test ./test/e2e -run '<name>' -v. The four
native cells have no test in this table and need none: the same dialect goes in and comes out, so
the translated route is not entered at all.
One cell, run: an OpenAI-format client on an Anthropic upstream
$ go test ./test/e2e/ -run '^TestCrossDialectResponseLossOnTheSameRow$' -v -count=1
=== RUN TestCrossDialectResponseLossOnTheSameRow
translate_response_test.go:260: EVIDENCE openai-client → anthropic-upstream
translate_response_test.go:262: EVIDENCE row: from=openai to=anthropic dropped=[logit_bias] approx=[max_tokens:defaulted-to-4096 usage.cache_write:unrepresentable-in-openai] pricing_key="anthropic/claude-haiku-4-5" usd=0.000356400
--- PASS: TestCrossDialectResponseLossOnTheSameRow (0.03s)
PASS
ok github.com/tokentriage/platform/test/e2e 0.071sThat run is from a checkout of the product at 3693f7bd, go1.25.6 on Windows, on 2026-08-24. One
logged line carrying the client’s response body is left out of the block above; it is read in full
on what a translation drops. The mock’s msg_mock_… id,
the source line numbers and the elapsed times belong to that machine. The from and to pair, the
dropped list, the pricing key and the dollar are functions of the tree and the vendored price
table, and the dollar is arithmetic over a mock’s token counts rather than anybody’s bill.
What crossing costs, pair by pair
The verdict table above says how to tell a translated row from a native one, and the test table says which test drives each cell. What differs between pairs is what the crossing costs, and three of the twelve carry something those tables cannot:
- OpenAI-format to
gemini. Six of Gemini’s finish reasons land oncontent_filterin the client’s vocabulary, and each of those mappings is recorded on the row as an approximation rather than passed off as a match. - OpenAI-format to
bedrock. A checksum mismatch on an upstream event is a stream failure rather than a drop, and it ends the response. - Anthropic-format to
openai. Usage arrives late. OpenAI emits its usage chunk after the finish chunk while the Anthropic wire carries usage onmessage_delta, so the encoder writes those numbers at finish time and records on the row that it did. The row is priced either way, so read the row rather than the streamed body if you need per-request usage; translated streams has both receipts.
On every translated pair, what a render could not carry is named on the row under its own name, and
what a translation drops is the per-target inventory. The
four native cells cost nothing: the same dialect goes in and comes out, so no render runs.
What a translated cell needs turned on
Every translated cell in the grid is off out of the box, and the two settings that arm it nest.
routing:
allow_cross_dialect: true # default false
allow_cross_dialect_streaming: true # default false; rejected without the line above
With allow_cross_dialect off, a routing rule whose tier points at a different-dialect upstream is
skipped and the request goes to the same-dialect default. With it on and
allow_cross_dialect_streaming off, the non-streaming cells work and a streaming request takes that
same fallback. A config that sets the streaming setting on its own is turned away at validation,
with both keys named in the message.
Whether a streaming pair is available is decided before the request is forwarded, not on the first frame, so a stream that cannot be translated becomes a same-dialect passthrough instead of dying halfway through.
Gemini-format and Bedrock-format clients
gemini and bedrock are columns in this grid and not rows. Either is reachable as an upstream
from both client formats, in both modes. Neither is read as a client format: a Gemini-format or
Bedrock-format request is not translated onto another dialect in this build.
The reason is evidence rather than difficulty. No population of Gemini-SDK or Bedrock-SDK clients pointed at a third-party gateway has been observed, and turning on a direction nobody has verified would ship an unowned surface rather than a feature.
A request in one of those formats is not rejected. It is recognised on its own native paths, and a
route that tries to send it across a dialect boundary gets a named error back from the translator
and falls open: the request is served byte-faithfully on its own dialect, which is a Gemini client
reaching a Gemini upstream and losing nothing at all. The row records decision.fail_safe: true and
carries no disclosures.translation block.
TestInboundDialectsAreDeferredNotBroken (internal/translate/targets_test.go) asserts both
halves: that the request is turned away, and that the error names the decision behind it. A reader
who meets a fail_safe row can trace it to that decision instead of filing a bug.
What to do instead: point your Gemini or Bedrock SDK at its own upstream through the gateway. You keep byte-faithful relay, routing, governance and a ledger row. What you give up is cross-dialect routing from that client, and the way to get it back is an OpenAI-format or Anthropic-format client, either of which reaches all four upstream dialects in both modes.
Why two rows and four columns
The two rows are not the whole of what the proxy accepts. Each shipped dialect is recognised on its
own native paths, so a client calling Deepgram’s /v1/listen or Anthropic’s /v1/messages is
parsed, governed and ledgered against that dialect’s default upstream. What is limited to two is the
client-format axis of translation: openai and anthropic are the two formats a request can be
read out of on the way to another dialect.
The four columns are narrower than the fan-out for a related reason. The proxy drives every dialect it ships; four of them have a translation adapter registered, and that is what makes a dialect a column here.
Fourteen wire-dialect driver packages ship in internal/dialect/, and all fourteen are compiled
into the running binary (cmd/tokentriage/dialects.go). The published coverage matrix says
eleven because its generator counts a driver only where the package directory name equals the
dialect id, and three do not — bfl/black_forest_labs, fal/fal_ai, runway/runwayml.
Eleven is the generator’s noun; fourteen is the directory’s. They describe the same code.
Where the grid stops
Four boundaries, none of which the tables above show.
- Every cell here was established on chat traffic. Nothing on this page states what a translated route does for another endpoint kind.
- The twelve tests run against mocks, shaped like each upstream’s wire, on a local run. A filled cell is a statement about the translation, not about a vendor’s live endpoint.
- Guardrail stream inspection and translated streaming cannot both be enabled. A config
carrying
allow_cross_dialect_streaming: trueand a guard that inspects at thestreamphase is turned away at validation, with the offending guard and phase named. Translated streams has the message and the two ways out of it. - A filled cell says the pair goes through, not that it goes through whole. Every crossing records what it could not express on the ledger row, and how much that amounts to differs per target.