Search

↑↓ to move↵ to openesc to closetop 6 documentation results

Book a demoTry TokenTriage

In development · Explore the product preview. Join the launch list or book a walkthrough.

Browse docs

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.

ReferenceLast updated

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.

Two client formats reach four upstream dialects, and the blank rows are a stated decisionThe grid is the claim. Two client formats, OpenAI and Anthropic, reach four upstream dialects, and each of those pairings works whether or not the response streams. That is sixteen cells across the top four rows: four where the client format and the upstream dialect are the same, so no translation happens, and twelve where the request is rendered into the upstream's dialect and the answer rendered back. Each of the twelve is driven by its own end-to-end test, and the streaming half of them needs two configuration flags rather than one, because cross-dialect streaming is gated separately and depends on cross-dialect translation being enabled at all. The two rows below the line are the ones the grid exists to explain. A Gemini-format or Bedrock-format client request finds no translation route to any other upstream. Those two dialects ship as upstream targets, which is why they hold the diagonal, and inbound translation from them was deferred rather than attempted. The blank cells are a stated product decision with a name attached, so a row that falls back is traceable to that decision instead of reading as a bug. What happens in a blank cell is worth saying plainly: the request is served on its own dialect, byte for byte, against the upstream that speaks it, and the row records the fall-back with no translation disclosure on it. For a streaming request the check runs before anything is forwarded, so a client gets an ordinary same-dialect answer rather than a stream that dies on its first frame.the client-format grid, drawnupstream dialectopenaianthropicgeminibedrockopenaiclient · not streamingnativetranslatedtranslatedtranslatedopenaiclient · streamingnativetranslatedtranslatedtranslatedanthropicclient · not streamingtranslatednativetranslatedtranslatedanthropicclient · streamingtranslatednativetranslatedtranslatedbelow the line: a wire format that is an upstream target, not a translation sourcegeminiclient · either modeno routeno routenativeno routebedrockclient · either modeno routeno routeno routenativeReading the gridtranslated — rendered into the upstream's dialect and back. Twelve cells, each with a named test.native — same dialect in and out, so no translation happens and nothing can be lost in one.no route — the request is served on its own dialect, byte for byte, and the row records thefall-back rather than a translation. Gemini and Bedrock ship as upstream targets; inbound was deferred.A streaming cell needs routing.allow_cross_dialect_streaming, which needs routing.allow_cross_dialect.

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.071s

That 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 on content_filter in 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 on message_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.

  1. Every cell here was established on chat traffic. Nothing on this page states what a translated route does for another endpoint kind.
  2. 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.
  3. Guardrail stream inspection and translated streaming cannot both be enabled. A config carrying allow_cross_dialect_streaming: true and a guard that inspects at the stream phase is turned away at validation, with the offending guard and phase named. Translated streams has the message and the two ways out of it.
  4. 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.