Skip to content

fix(transport): send the translated body to the endpoint that speaks its format (#3418, #3439) - #3538

Closed
ntdat812 wants to merge 1 commit into
decolua:masterfrom
ntdat812:fix/transport-body-format-alignment
Closed

ntdat812 wants to merge 1 commit into
decolua:masterfrom
ntdat812:fix/transport-body-format-alignment

Conversation

@ntdat812

Copy link
Copy Markdown
Contributor

Closes #3418. Closes #3439.

Problem

chatCore picks the upstream transport from the client's sourceFormat, then lets a model-level targetFormat override the format the body is written in — but not the transport:

const useTransport = (!modelSupportedFormats || modelSupportedFormats.includes(sourceFormat)) ? runtimeTransport : null;
const targetFormat = modelTargetFormat || useTransport?.format || getTargetFormat(provider, credentials);
if (useTransport && credentials) credentials.runtimeTransport = useTransport;

runtimeTransport is what DefaultExecutor.buildUrl and buildHeaders read for the URL, the headers and the auth descriptor. So for an OpenAI-format request to a model pinned to claude, 9Router serializes a Claude body and posts it to the provider's OpenAI endpoint with that endpoint's bearer auth.

Upstreams parse what they recognize and drop the rest, so the failure is quiet and looks like a model problem:

Fix

Resolve the format and the transport in one place so they cannot disagree, and let the transport follow targetFormat when a model pins one:

const transport = modelTargetFormat ? resolveTransport(provider, modelTargetFormat) : useTransport;

Three models declare targetFormat on a provider that has more than one endpoint:

model before after
minimax/MiniMax-M3 claude body → /v1/chat/completions claude body → /anthropic/v1/messages
minimax-cn/MiniMax-M3 claude body → /v1/chat/completions claude body → /anthropic/v1/messages
xiaomi-tokenplan/mimo-v2.5-pro-claude claude body → /chat/completions, bearer claude body → /anthropic/v1/messages, x-api-key

Nothing else changes: without a model-level targetFormat the expression is the previous useTransport, and the per-model supportedFormats guard is untouched — a model that does not declare a format still never gets that transport.

I did not take the other half of #3439's proposal (dropping targetFormat: "claude" from MiniMax-M3). That is a product decision about which wire format M3 should speak, and it is not needed to fix either report — the Anthropic endpoint handles both the image and the tool schema, as the #3418 control tests show. Same reasoning for mimo-v2.5-pro-claude, whose whole point is to be Claude-native.

Verification

tests/unit/upstream-route-alignment.test.js derives the model list from the registry rather than hardcoding it, so a future pinned model is covered automatically (with a guard test so the table can't pass vacuously).

Reverting the one-line fix to const transport = useTransport; fails exactly the three alignment cases:

FAIL  minimax/MiniMax-M3: openai client → claude body on the claude transport
FAIL  minimax-cn/MiniMax-M3: openai client → claude body on the claude transport
FAIL  xiaomi-tokenplan/mimo-v2.5-pro-claude: openai client → claude body on the claude transport
AssertionError: expected 'openai' to be 'claude'
Tests  3 failed | 9 passed (12)

Full offline suite (vitest run unit auth translator, CI=true, excluding real/), same command on both sides:

tests failed
origin/master (699edac) 1910 95
this branch 1922 95

The failure sets are identical — comm on the sorted full test names returns nothing in either direction. The 95 are pre-existing (windsurf/kimi-coding registry drift and their snapshots) and unrelated to this change. eslint clean on all three files.

I have no MiniMax or Xiaomi Token Plan credentials, so I have not run a live request; what is verified here is that the body format and the endpoint now agree, which is the mechanism both issues identify.

…its format

A model-level `targetFormat` overrides the transport chosen for the client's
format, but `credentials.runtimeTransport` kept pointing at the sourceFormat
match. The transport decides the URL, the headers and the auth scheme, so an
OpenAI-format request for a model pinned to `claude` was serialized as a Claude
body and posted to /v1/chat/completions with bearer auth.

Upstreams accept what they can parse and drop the rest, so this fails quietly:
image blocks vanish (decolua#3418) and Claude tool schemas come back as
`invalid tool type (2013)` (decolua#3439).

Resolve the format and the transport together so they cannot diverge. Three
models are pinned today — minimax/MiniMax-M3, minimax-cn/MiniMax-M3 and
xiaomi-tokenplan/mimo-v2.5-pro-claude — and all three now reach the Anthropic
endpoint their body is written for.

The per-model transport guard is unchanged: a model that does not declare a
format still never gets that transport.
@ntdat812

ntdat812 commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Closing this: master has since decided the opposite of what it proposes, deliberately and with a documented reason.

This PR pinned the endpoint to a model's declared targetFormat for every client wire format, so that the body format and the endpoint could never diverge. open-sse/handlers/chatCore.js on current master now resolves it the other way round:

// A source-format-matched endpoint keeps the request lossless. Prefer it
// over a model-level targetFormat, which is only the fallback for clients
// whose wire format has no supported transport (for example MiniMax-M3:
// OpenAI clients should stay on /chat/completions; other clients can fall
// back to its declared Claude target).
const targetFormat = useTransport?.format || modelTargetFormat || getTargetFormat(provider, credentials);

tests/unit/upstream-route-alignment.test.js in this branch asserts that an OpenAI client on minimax/MiniMax-M3 lands on the Claude transport — which is exactly the case that comment singles out as wrong. Rebasing would therefore not resolve a text conflict; it would quietly revert a design decision, and the tests shipped here would enforce the revert. That is not a call for a PR to make on its own.

The per-model supportedFormats guard this PR also introduced is already on master. What is left unaddressed is the narrower invariant behind the original report — that when a model-level targetFormat is the one chosen, the transport should move with it rather than staying on the source-format endpoint. If that is still worth having, it is a much smaller change on top of the current precedence, and I am happy to open it as such.

@ntdat812 ntdat812 closed this Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant