Skip to content

fix(claude): keep thinking out of visible content - #2190

Open
Jordannst wants to merge 1 commit into
decolua:masterfrom
Jordannst:investigate/reasoning-leak-2158
Open

Jordannst wants to merge 1 commit into
decolua:masterfrom
Jordannst:investigate/reasoning-leak-2158

Conversation

@Jordannst

Copy link
Copy Markdown
Contributor

Description

Fixes a Claude-to-OpenAI streaming translation leak where Claude thinking blocks were emitted both as OpenAI-compatible reasoning_content and as visible content chunks containing <think> / </think> markers.

Root cause

In open-sse/translator/response/claude-to-openai.js, the Claude thinking block lifecycle emitted synthetic OpenAI content chunks:

  • content_block_start for thinking emitted delta.content: "<think>"
  • content_block_delta correctly emitted delta.reasoning_content
  • content_block_stop emitted delta.content: "</think>"

This meant OpenAI-compatible clients could receive reasoning markers through normal visible content, even though the actual thinking text was already carried separately in reasoning_content.

Changes

  • Stop emitting synthetic <think> / </think> chunks into delta.content for Claude thinking blocks.
  • Keep thinking_delta mapped to delta.reasoning_content, preserving reasoning for clients/providers that support it.
  • Add a regression test covering a Claude stream with thinking followed by visible text.

Verification

  • npx vitest run tests/unit/claude-reasoning-response.test.js
  • git diff --check

The regression test verifies:

  • visible content only contains the final answer text
  • no delta.content chunk is <think> or </think>
  • thinking text remains available through reasoning_content

Notes

There is a separate non-streaming cleanup opportunity around how mixed content + reasoning_content responses are handled, but this PR intentionally keeps the scope limited to the streaming leak reported in #2158.

Closes #2158

bloodf pushed a commit to bloodf/durindoor that referenced this pull request Jul 10, 2026
Translator regression asserting a Claude->OpenAI stream surfaces reasoning via
the reasoning channel and never emits literal <think>/</think> text in
delta.content. Logic (reasoningDelta channel, no think-tag content deltas)
already present in dev; this locks the behavior.

Ported from decolua/9router#2190 @ dc417f9b3f
bloodf pushed a commit to bloodf/durindoor that referenced this pull request Jul 10, 2026
Translator regression asserting a Claude->OpenAI stream surfaces reasoning via
the reasoning channel and never emits literal <think>/</think> text in
delta.content. Logic (reasoningDelta channel, no think-tag content deltas)
already present in dev; this locks the behavior.

Ported from decolua/9router#2190 @ dc417f9b3f
bloodf pushed a commit to bloodf/durindoor that referenced this pull request Jul 10, 2026
Translator regression asserting a Claude->OpenAI stream surfaces reasoning via
the reasoning channel and never emits literal <think>/</think> text in
delta.content. Logic (reasoningDelta channel, no think-tag content deltas)
already present in dev; this locks the behavior.

Ported from decolua/9router#2190 @ dc417f9b3f
bloodf added a commit to bloodf/durindoor that referenced this pull request Jul 10, 2026
* port(upstream): #2237 - salvage orphaned tool results across formats

Non-lossy salvageOrphanedToolResults folds orphan tool output into user text
(`[Tool result: ...]`) instead of dropping it, across messages[] (OpenAI role:tool,
Claude tool_result blocks) and contents[] (Gemini/Antigravity functionResponse).
Runs unconditionally in the request pipeline and after each compression stage
(RTK/Headroom/PXPIPE) with fixMissingToolResponses to restore the tool-pairing
invariant. Responses API function_call_output stays structurally stripped in
openai-responses.js. Removes the obsolete shouldStripOrphanedToolResults gate;
Gemini-family standalone functionResponse (no functionCall) is preserved.

Ported from decolua/9router#2237 @ a32bda9e44

* port(upstream): #2279 - test doubled tool args collapse openai->claude

Translator regression asserting the OpenAI->Claude response translator
deduplicates doubled JSON tool arguments (same object emitted twice) into a
single parseable input_json_delta, and emits message_stop exactly once.

Logic (claudeFinishHandled finish guard + deduplicateDoubledJson) already
present in dev; this locks the behavior.

Ported from decolua/9router#2279 @ 1c9ad466ed

* port(upstream): #2190 - test thinking stays out of visible content

Translator regression asserting a Claude->OpenAI stream surfaces reasoning via
the reasoning channel and never emits literal <think>/</think> text in
delta.content. Logic (reasoningDelta channel, no think-tag content deltas)
already present in dev; this locks the behavior.

Ported from decolua/9router#2190 @ dc417f9b3f

* port(upstream): #2318 - test strip Responses-only fields before forward

Translator regression asserting Responses->Chat translation drops
client_metadata/background/truncation (Responses-API-only fields rejected by
third-party chat providers with HTTP 400) while the plain OPENAI->OPENAI path
preserves them (negative control). Logic already present in dev; locks behavior.

Ported from decolua/9router#2318 @ edb20e143d

* fix(translator): skip salvage on native Gemini contents[]

restore the Gemini-family (gemini/gemini-cli/antigravity/vertex) guard
around salvageOrphanedToolResults in the translateRequest pipeline. the
body still carries native contents[] at that point; salvage keys known
functionCalls globally and rewrites any functionResponse whose id is not
in that set into '[Tool result: ...]' text, dropping legitimate tool
results before gemini->openai conversion can read them.

fixes CI run 29122517702 (tests/unit/gemini-to-openai-function-response).

---------

Co-authored-by: CortexOS <cortexos@localhost>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: reasoning_content leaks into content for kr/* Claude models (v0.5.12)

1 participant