◐ Off-By-One · answer catalog

anthropic-client-nonstream-upstream-format-leak

2 answer(s)javascriptjavascript

Problem class: anthropic-client-nonstream-upstream-format-leak

📦 Source in repository (JSON)

Answer 1

I diagnosed the issue against the live repo, reproduced it, and verified the fix. The solution is saved at ~/solution.md. Here it is:


Fix: Anthropic (/v1/messages) non-streaming client receives an OpenAI chat.completion body

Problem 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

1. Symptom

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.

2. Root-cause analysis

translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) converted the upstream body per branch. The branches that matter ignored the client format (sourceFormat):

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.

3. The fix

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

4. Regression tests

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

5. Verification

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.

6. Takeaways

  1. Check the other transport before patching a format path — streaming was already correct via the registry pivot; copy its architecture, not its output.
  2. Client-format adaptation belongs at the single exit, never per branch — duplicated conversions get missed by the next branch.
  3. Idempotence is the safety net — gate on the shape that needs fixing (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 & signatures

# 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": ""}

Answer 2

I diagnosed the issue against the live repo, reproduced it, and verified the fix. The solution is saved at ~/solution.md. Here it is:


Fix: Anthropic (/v1/messages) non-streaming client receives an OpenAI chat.completion body

Problem 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

1. Symptom

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.

2. Root-cause analysis

translateNonStreamingResponse(responseBody, targetFormat, sourceFormat) converted the upstream body per branch. The branches that matter ignored the client format (sourceFormat):

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.

3. The fix

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

4. Regression tests

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

5. Verification

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.

6. Takeaways

  1. Check the other transport before patching a format path — streaming was already correct via the registry pivot; copy its architecture, not its output.
  2. Client-format adaptation belongs at the single exit, never per branch — duplicated conversions get missed by the next branch.
  3. Idempotence is the safety net — gate on the shape that needs fixing (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 & signatures

# 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": ""}
Generated from the verified corpus · MIT licensedBack to the catalog