Problem class: anthropic-client-nonstream-upstream-format-leak
I diagnosed the issue against the live repo, reproduced it, and verified the fix. The solution is saved at ~/solution.md. Here it is:
/v1/messages) non-streaming client receives an OpenAI chat.completion bodyProblem class: anthropic-client-nonstream-upstream-format-leak
Repo: totalwindupflightsystems/9router (branch federation)
Task: DF-9ROUTER-25 · Fix commit: af3c3686
Files: open-sse/handlers/chatCore/nonStreamingHandler.js, tests/unit/claude-client-nonstream-format.test.js
A non-streaming POST /v1/messages (Anthropic client format) answered HTTP 200 with an OpenAI body:
{ "id": "chatcmpl-...", "object": "chat.completion",
"choices": [{ "message": { "content": "pong" }, "finish_reason": "stop" }], "usage": {} }
instead of { "type": "message", "content": [{ "type": "text", "text": "pong" }], "usage": { "input_tokens": 2017, "output_tokens": 3 } }.
An Anthropic client reads content[], not choices[], so it saw a silent empty answer (no error). Reproduced live against ollama-local (native /api/chat, Ollama JSON, gemma3:4b). The streaming path was already correct; only stream:false leaked.
translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) converted the upstream body per branch. The branches that matter ignored the client format (sourceFormat):
targetFormat === FORMATS.OLLAMA → returned ollamaBodyToOpenAI(body) — an OpenAI chat.completion.targetFormat === GEMINI | ANTIGRAVITY | GEMINI_CLI | VERTEX → built an OpenAI chat.completion inline.Neither checked sourceFormat === FORMATS.CLAUDE. Only the targetFormat === OPENAI && sourceFormat === CLAUDE branch and the forced-SSE-to-JSON paths produced Anthropic shape.
Orientation note: signature is (body, targetFormat = upstream, sourceFormat = client). A Claude client on an Ollama upstream is targetFormat = OLLAMA, sourceFormat = CLAUDE.
Why it recurs: the conversion lived inside each per-upstream branch, so every new branch/provider must remember it. The streaming path proves the correct design is one pivoted exit.
Do client-format adaptation once at the single exit, after all branches. Rename the branch function to translateUpstreamResponse() and compose it with an idempotent toClientFormat() adapter reusing the existing claudeMessageFromChatCompletion().
-export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
+function translateUpstreamResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
if (targetFormat === sourceFormat) return responseBody;
...
return responseBody;
}
+/**
+ * Adapt a translated body to the CLIENT's format — applied ONCE, at the single
+ * exit of `translateNonStreamingResponse`, after every upstream branch.
+ * Only an OpenAI-shaped result is converted (`choices[]` or
+ * `object:"chat.completion"`), so an Anthropic-shaped body passes through
+ * untouched (idempotent).
+ */
+function toClientFormat(body, sourceFormat) {
+ if (sourceFormat !== FORMATS.CLAUDE) return body;
+ const isOpenAIShaped = Array.isArray(body?.choices) || body?.object === "chat.completion";
+ return isOpenAIShaped ? claudeMessageFromChatCompletion(body) : body;
+}
+
+export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
+ return toClientFormat(
+ translateUpstreamResponse(responseBody, targetFormat, sourceFormat, customToolNames),
+ sourceFormat
+ );
+}
Why it's safe: single choke point for present/future branches; idempotent shape-gated conversion; non-Claude clients (sourceFormat !== CLAUDE) pass through verbatim.
Apply: git cherry-pick af3c3686 (or the manual rename + wrapper above).
tests/unit/claude-client-nonstream-format.test.js — 8 cases. Cases (a) Ollama→Anthropic, (b) Ollama tool_calls→tool_use with parsed input, (c) Gemini→Anthropic are the bug; (d) OpenAI client still gets chat.completion from Ollama, (e) Claude upstream still yields choices[] for an OpenAI client, (f) chat-native→Anthropic, plus two guards (Claude body untouched for Claude client / unmapped target) pin unchanged behavior. (Full file is in ~/solution.md.)
cd tests && npm install
./node_modules/.bin/vitest run unit/claude-client-nonstream-format.test.js
RED (pre-fix af3c368^), observed:
❯ unit/claude-client-nonstream-format.test.js (8 tests | 3 failed) 10ms
× (a) converts an Ollama-shaped upstream body into an Anthropic message
× (b) converts Ollama tool_calls into Anthropic tool_use blocks with parsed input
× (c) converts a Gemini-shaped upstream body into an Anthropic message
AssertionError: expected undefined to be 'message'
Tests 3 failed | 5 passed (8)
Exactly the 3 Claude-client cases fail; the guards already pass.
GREEN (post-fix af3c3686), observed:
Test Files 1 passed (1)
Tests 8 passed (8)
Lint: eslint on both files → exit 0, 0 problems.
Live before/after (ollama-local, gemma3:4b, stream:false):
POST /v1/messages body |
|
|---|---|
| pre-fix | { "object": "chat.completion", "choices": [ ... ] } |
| post-fix | { "type":"message","role":"assistant","content":[{"type":"text","text":"pong\n"}],"stop_reason":"end_turn","usage":{"input_tokens":2017,"output_tokens":3} } |
/v1/chat/completions unchanged. Full suite: 2726 tests / 2582 pass / 85 fail (inside the 93-item known-fails baseline) / 59 pending; verify-no-regression exit 0.
choices[] / object:"chat.completion") so already-correct Anthropic bodies pass through.Verified locally: RED 3/8 fail on the pre-fix file, GREEN 8/8 after, eslint clean.
# Evidence - Problem class: anthropic-client-nonstream-upstream-format-leak - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T00:08:22.553Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a non-streaming POST /v1/messages (Anthropic/Claude client format) answered HTTP 200 with an OpenAI body: {id:chatcmpl-..., object:\"chat.completion\", choices:[{message:{content:...}}], usage:{}} instead of {type:\"message\", content:[{type:\"text\",...}], usage:{input_tokens,output_tokens}}. An Anthropic client reads content[] and never choices[], so it saw a silent empty answer, not an error. Reproduced live with the ollama-local provider (Ollama gemma3:4b). ROOT CAUSE: translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) converted the upstream body per-branch and ignored the client format on the branches that matter \u2014 targetFormat===FORMATS.OLLAMA returned ollamaBodyToOpenAI(body) (an OpenAI chat.completion object) and the GEMINI/ANTIGRAVITY/GEMINI_CLI/VERTEX branch built the same OpenAI object; neither consulted sourceFormat===FORMATS.CLAUDE. Only the targetFormat===OPENAI && sourceFormat===CLAUDE branch and the forced-SSE-to-JSON paths (sseToJsonHandler.js) converted to Anthropic shape. The STREAMING path was already correct because translateResponse pivots through the response-translator registry (ollama->openai->claude), which is why the bug only showed on stream:false. FIX (structural, single exit): rename the per-branch function to translateUpstreamResponse() and make the exported translateNonStreamingResponse() compose it with a new toClientFormat(body, sourceFormat) adapter \u2014 if sourceFormat===CLAUDE and the result is OpenAI-shaped (Array.isArray(body?.choices) || body?.object === 'chat.completion'), pass it through the existing claudeMessageFromChatCompletion(); Anthropic-shaped bodies pass through untouched, so the adapter is idempotent and no branch (present or future) can leak chat.completion to a Claude client. Applying the conversion inside each branch is exactly how this class recurs. VERIFICATION: new tests/unit/claude-client-nonstream-format.test.js (8 cases: Ollama body -> type=message/content[0]={type:text,text:pong}/usage numbers/no choices; Ollama tool_calls -> tool_use with parsed input; Gemini body -> Anthropic message; regression guards that an OpenAI client still gets chat.completion from an Ollama upstream and that a Claude upstream still yields choices[] for an OpenAI client). RED proved by swapping in the pre-fix file (3 of 8 fail: the three Claude-client cases) and GREEN 8/8 after; live pre-fix vs post-fix bodies captured: pre {object:chat.completion, choices[]} -> post {type:message, role:assistant, content:[{type:text,text:\"pong\\n\"}], stop_reason:end_turn, usage:{input_tokens:2017, output_tokens:3}}; /v1/chat/completions unchanged. Full suite 2726 tests / 2582 pass / 85 fail (all inside the 93-item known-fails baseline) / 59 pending, verify-no-regression exit 0; eslint 0 problems. PRE-SOLVE USEFULNESS: check the OTHER transport before patching a format path (the streaming arm was already correct via the registry pivot) and put client-format adaptation at the single exit of the translator, never inside each upstream branch.", "environment": "Next.js 16 plain-JS ESM gateway (9router fork, branch federation); POST /v1/messages declared Claude format by detectFormatByEndpoint(); non-streaming path open-sse/handlers/chatCore/nonStreamingHandler.js; upstream = host Ollama reached through the ollama-local provider (native /api/chat, Ollama JSON shape).", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "anthropic-client-nonstream-upstream-format-leak", "provider": "openrouter", "solved_at": "2026-09-17T00:08:22.553Z", "version": ""}I diagnosed the issue against the live repo, reproduced it, and verified the fix. The solution is saved at ~/solution.md. Here it is:
/v1/messages) non-streaming client receives an OpenAI chat.completion bodyProblem class: anthropic-client-nonstream-upstream-format-leak
Repo: totalwindupflightsystems/9router (branch federation)
Task: DF-9ROUTER-25 · Fix commit: af3c3686
Files: open-sse/handlers/chatCore/nonStreamingHandler.js, tests/unit/claude-client-nonstream-format.test.js
A non-streaming POST /v1/messages (Anthropic client format) answered HTTP 200 with an OpenAI body:
{ "id": "chatcmpl-...", "object": "chat.completion",
"choices": [{ "message": { "content": "pong" }, "finish_reason": "stop" }], "usage": {} }
instead of { "type": "message", "content": [{ "type": "text", "text": "pong" }], "usage": { "input_tokens": 2017, "output_tokens": 3 } }.
An Anthropic client reads content[], not choices[], so it saw a silent empty answer (no error). Reproduced live against ollama-local (native /api/chat, Ollama JSON, gemma3:4b). The streaming path was already correct; only stream:false leaked.
translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) converted the upstream body per branch. The branches that matter ignored the client format (sourceFormat):
targetFormat === FORMATS.OLLAMA → returned ollamaBodyToOpenAI(body) — an OpenAI chat.completion.targetFormat === GEMINI | ANTIGRAVITY | GEMINI_CLI | VERTEX → built an OpenAI chat.completion inline.Neither checked sourceFormat === FORMATS.CLAUDE. Only the targetFormat === OPENAI && sourceFormat === CLAUDE branch and the forced-SSE-to-JSON paths produced Anthropic shape.
Orientation note: signature is (body, targetFormat = upstream, sourceFormat = client). A Claude client on an Ollama upstream is targetFormat = OLLAMA, sourceFormat = CLAUDE.
Why it recurs: the conversion lived inside each per-upstream branch, so every new branch/provider must remember it. The streaming path proves the correct design is one pivoted exit.
Do client-format adaptation once at the single exit, after all branches. Rename the branch function to translateUpstreamResponse() and compose it with an idempotent toClientFormat() adapter reusing the existing claudeMessageFromChatCompletion().
-export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
+function translateUpstreamResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
if (targetFormat === sourceFormat) return responseBody;
...
return responseBody;
}
+/**
+ * Adapt a translated body to the CLIENT's format — applied ONCE, at the single
+ * exit of `translateNonStreamingResponse`, after every upstream branch.
+ * Only an OpenAI-shaped result is converted (`choices[]` or
+ * `object:"chat.completion"`), so an Anthropic-shaped body passes through
+ * untouched (idempotent).
+ */
+function toClientFormat(body, sourceFormat) {
+ if (sourceFormat !== FORMATS.CLAUDE) return body;
+ const isOpenAIShaped = Array.isArray(body?.choices) || body?.object === "chat.completion";
+ return isOpenAIShaped ? claudeMessageFromChatCompletion(body) : body;
+}
+
+export function translateNonStreamingResponse(responseBody, targetFormat, sourceFormat, customToolNames = null) {
+ return toClientFormat(
+ translateUpstreamResponse(responseBody, targetFormat, sourceFormat, customToolNames),
+ sourceFormat
+ );
+}
Why it's safe: single choke point for present/future branches; idempotent shape-gated conversion; non-Claude clients (sourceFormat !== CLAUDE) pass through verbatim.
Apply: git cherry-pick af3c3686 (or the manual rename + wrapper above).
tests/unit/claude-client-nonstream-format.test.js — 8 cases. Cases (a) Ollama→Anthropic, (b) Ollama tool_calls→tool_use with parsed input, (c) Gemini→Anthropic are the bug; (d) OpenAI client still gets chat.completion from Ollama, (e) Claude upstream still yields choices[] for an OpenAI client, (f) chat-native→Anthropic, plus two guards (Claude body untouched for Claude client / unmapped target) pin unchanged behavior. (Full file is in ~/solution.md.)
cd tests && npm install
./node_modules/.bin/vitest run unit/claude-client-nonstream-format.test.js
RED (pre-fix af3c368^), observed:
❯ unit/claude-client-nonstream-format.test.js (8 tests | 3 failed) 10ms
× (a) converts an Ollama-shaped upstream body into an Anthropic message
× (b) converts Ollama tool_calls into Anthropic tool_use blocks with parsed input
× (c) converts a Gemini-shaped upstream body into an Anthropic message
AssertionError: expected undefined to be 'message'
Tests 3 failed | 5 passed (8)
Exactly the 3 Claude-client cases fail; the guards already pass.
GREEN (post-fix af3c3686), observed:
Test Files 1 passed (1)
Tests 8 passed (8)
Lint: eslint on both files → exit 0, 0 problems.
Live before/after (ollama-local, gemma3:4b, stream:false):
POST /v1/messages body |
|
|---|---|
| pre-fix | { "object": "chat.completion", "choices": [ ... ] } |
| post-fix | { "type":"message","role":"assistant","content":[{"type":"text","text":"pong\n"}],"stop_reason":"end_turn","usage":{"input_tokens":2017,"output_tokens":3} } |
/v1/chat/completions unchanged. Full suite: 2726 tests / 2582 pass / 85 fail (inside the 93-item known-fails baseline) / 59 pending; verify-no-regression exit 0.
choices[] / object:"chat.completion") so already-correct Anthropic bodies pass through.Verified locally: RED 3/8 fail on the pre-fix file, GREEN 8/8 after, eslint clean.
# Evidence - Problem class: anthropic-client-nonstream-upstream-format-leak - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-17T00:08:22.553Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "SYMPTOM: a non-streaming POST /v1/messages (Anthropic/Claude client format) answered HTTP 200 with an OpenAI body: {id:chatcmpl-..., object:\"chat.completion\", choices:[{message:{content:...}}], usage:{}} instead of {type:\"message\", content:[{type:\"text\",...}], usage:{input_tokens,output_tokens}}. An Anthropic client reads content[] and never choices[], so it saw a silent empty answer, not an error. Reproduced live with the ollama-local provider (Ollama gemma3:4b). ROOT CAUSE: translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) converted the upstream body per-branch and ignored the client format on the branches that matter \u2014 targetFormat===FORMATS.OLLAMA returned ollamaBodyToOpenAI(body) (an OpenAI chat.completion object) and the GEMINI/ANTIGRAVITY/GEMINI_CLI/VERTEX branch built the same OpenAI object; neither consulted sourceFormat===FORMATS.CLAUDE. Only the targetFormat===OPENAI && sourceFormat===CLAUDE branch and the forced-SSE-to-JSON paths (sseToJsonHandler.js) converted to Anthropic shape. The STREAMING path was already correct because translateResponse pivots through the response-translator registry (ollama->openai->claude), which is why the bug only showed on stream:false. FIX (structural, single exit): rename the per-branch function to translateUpstreamResponse() and make the exported translateNonStreamingResponse() compose it with a new toClientFormat(body, sourceFormat) adapter \u2014 if sourceFormat===CLAUDE and the result is OpenAI-shaped (Array.isArray(body?.choices) || body?.object === 'chat.completion'), pass it through the existing claudeMessageFromChatCompletion(); Anthropic-shaped bodies pass through untouched, so the adapter is idempotent and no branch (present or future) can leak chat.completion to a Claude client. Applying the conversion inside each branch is exactly how this class recurs. VERIFICATION: new tests/unit/claude-client-nonstream-format.test.js (8 cases: Ollama body -> type=message/content[0]={type:text,text:pong}/usage numbers/no choices; Ollama tool_calls -> tool_use with parsed input; Gemini body -> Anthropic message; regression guards that an OpenAI client still gets chat.completion from an Ollama upstream and that a Claude upstream still yields choices[] for an OpenAI client). RED proved by swapping in the pre-fix file (3 of 8 fail: the three Claude-client cases) and GREEN 8/8 after; live pre-fix vs post-fix bodies captured: pre {object:chat.completion, choices[]} -> post {type:message, role:assistant, content:[{type:text,text:\"pong\\n\"}], stop_reason:end_turn, usage:{input_tokens:2017, output_tokens:3}}; /v1/chat/completions unchanged. Full suite 2726 tests / 2582 pass / 85 fail (all inside the 93-item known-fails baseline) / 59 pending, verify-no-regression exit 0; eslint 0 problems. PRE-SOLVE USEFULNESS: check the OTHER transport before patching a format path (the streaming arm was already correct via the registry pivot) and put client-format adaptation at the single exit of the translator, never inside each upstream branch.", "environment": "Next.js 16 plain-JS ESM gateway (9router fork, branch federation); POST /v1/messages declared Claude format by detectFormatByEndpoint(); non-streaming path open-sse/handlers/chatCore/nonStreamingHandler.js; upstream = host Ollama reached through the ollama-local provider (native /api/chat, Ollama JSON shape).", "language": "javascript", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "anthropic-client-nonstream-upstream-format-leak", "provider": "openrouter", "solved_at": "2026-09-17T00:08:22.553Z", "version": ""}