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

Source: https://tokentriage.com/docs/build/providers-dialect-matrix

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';
import DialectMatrixDiagram from '../../../components/DialectMatrixDiagram.astro';

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](/docs/build/providers-cross-dialect), what a
crossing costs you is on
[What a translation drops, and how you find out](/docs/build/providers-translation-loss), and what a
translated stream does when a frame fails halfway through is on
[Translated streams](/docs/build/providers-streaming-translation).

## 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](#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.

<DialectMatrixDiagram />

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.

<Details summary="One cell, run: an OpenAI-format client on an Anthropic upstream">

```console
$ 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](/docs/build/providers-translation-loss). 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.

</Details>

## 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](/docs/build/providers-streaming-translation) 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](/docs/build/providers-translation-loss) 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.

```yaml
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.

<Callout type="warning">
An upstream that a translated route targets needs `dialect:` named on it as well as `provider:`.
The route reads the target dialect from the config rather than from the compiled descriptor, so an
upstream naming `provider:` alone leaves that field empty, no cell in this grid is entered, and the
request reaches the upstream untranslated. That is a defect in the shipped build;
[Cross-dialect translation](/docs/build/providers-cross-dialect) carries it with the two captures
that establish it.
</Callout>

## 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](/docs/build/providers-streaming-translation) 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.

<NextSteps items={[
  {title: "What a translation drops, and how you find out", href: "/docs/build/providers-translation-loss", description: "A cell in this grid says the pair works. This says what it cost you, field by field."},
  {title: "Translated streams", href: "/docs/build/providers-streaming-translation", description: "The stream rows in this grid have a failure mode the non-stream rows do not."},
  {title: "Cross-dialect translation", href: "/docs/build/providers-cross-dialect", description: "How to turn one of these cells on, and the config shape it needs to stay on."},
  {title: "Wire dialects", href: "/docs/build/providers-dialects", description: "Why two of these dialects are columns here and not rows."}
]} />
