Skip to content

fix: signature + correct field for streaming reasoning thinking blocks - #17

Open
angelz07 wants to merge 1 commit into
nielspeter:mainfrom
angelz07:fix/reasoning-thinking-signature
Open

fix: signature + correct field for streaming reasoning thinking blocks#17
angelz07 wants to merge 1 commit into
nielspeter:mainfrom
angelz07:fix/reasoning-thinking-signature

Conversation

@angelz07

Copy link
Copy Markdown

What

Fix empty Claude Code responses from reasoning models exposed via the OpenAI-style reasoning_content field (LM Studio local models, OpenAI o1/o3).

Why

When a backend streams reasoning via delta.reasoning_content, the proxy produced a thinking block that Claude Code silently discarded, leaving the user with an empty response even though the model returned valid text. Two defects in streamOpenAIToClaude:

  1. The thinking_delta used the field text instead of thinking, so the reasoning payload was malformed. The OpenRouter reasoning_details path already used thinking correctly.
  2. No signature_delta was ever emitted, and the thinking block was closed only at stream end — after the text block. Claude Code discards thinking blocks that arrive without a signature (see the pitfall documented in CLAUDE.md), so the whole assistant turn was dropped.

How

Add a reusable closeThinkingBlock() helper that emits a signature_delta followed by content_block_stop, and call it before any text/tool block starts and once more at stream end. This finalizes the thinking block in the order Claude Code expects:

content_block_start(thinking) -> thinking_delta* -> signature_delta -> content_block_stop -> content_block_start(text) -> ...

The signature value is a fixed opaque string; it is never validated against Anthropic for local/OpenAI-compatible providers, and Claude Code only requires its presence to treat the block as valid thinking.

Testing

  • go test ./internal/server/... passes.
  • Verified end-to-end against a local LM Studio (Qwen3 reasoning model): raw Claude SSE now shows thinking -> signature_delta -> stop -> text ordering with the text content preserved, where before the thinking block had no signature and the wrong delta field.

🤖 Generated with Claude Code

…blocks

Reasoning models exposed via OpenAI-style `reasoning_content` (e.g. LM Studio,
o1/o3) produced empty responses in Claude Code. Two defects in the streaming
path:

1. The thinking_delta used the field `text` instead of `thinking`, so the
   reasoning payload was malformed (the OpenRouter path already used `thinking`).

2. No `signature_delta` was ever emitted and the thinking block was closed only
   at stream end, after the text block. Claude Code discards thinking blocks that
   arrive without a signature, so the whole assistant turn was dropped.

Add a reusable closeThinkingBlock() helper that emits a signature_delta followed
by content_block_stop, and call it before any text/tool block starts and at
stream end. This finalizes the thinking block in the order Claude Code expects
(thinking -> signature -> stop -> text). The signature value is opaque for
local/OpenAI-compatible providers (never validated against Anthropic).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.

2 participants