fix(llm): close text block before starting thinking block in inbound stream#1677
Conversation
…stream When an upstream provider (observed with PackyCode proxy) returns a text content block before the thinking block (text → thinking order instead of the typical thinking → text), the inbound stream transformer failed to close the text block before opening the thinking block. This caused both blocks to share the same index, resulting in Claude Code reporting "Content block not found" errors. The fix adds a check for `hasTextContentStarted` in the reasoning content handler, mirroring the existing `hasToolContentStarted` check, ensuring the text block is properly closed (with citations flushed) before the thinking block begins. Closes looplj#1655
There was a problem hiding this comment.
Code Review
This pull request updates the Anthropic inbound stream handler to properly manage content block transitions when reasoning content is received. Specifically, it adds logic to flush citations and close an active text content block before processing thinking deltas. Feedback suggests refactoring this block-closing logic into a helper method, as the same sequence of operations is now duplicated in several places, increasing the risk of maintenance errors.
| if s.hasTextContentStarted { | ||
| if err := s.flushPendingTextCitations(); err != nil { | ||
| s.err = fmt.Errorf("failed to flush text citations before thinking: %w", err) | ||
| return false | ||
| } | ||
|
|
||
| s.hasTextContentStarted = false | ||
|
|
||
| if err := s.enqueEvent(&StreamEvent{ | ||
| Type: "content_block_stop", | ||
| Index: &s.contentIndex, | ||
| }); err != nil { | ||
| s.err = fmt.Errorf("failed to enqueue content_block_stop event: %w", err) | ||
| return false | ||
| } | ||
|
|
||
| s.contentIndex += 1 | ||
| } |
There was a problem hiding this comment.
The logic for closing a text content block—which involves flushing citations, resetting the state flag, enqueuing the stop event, and incrementing the index—is now duplicated in five different locations within this file (lines 131, 332, 446, 566, and 701). Since the order of operations is critical (for instance, flushPendingTextCitations must be called before hasTextContentStarted is set to false to avoid an early return), this duplication is error-prone. Consider refactoring this into a helper method like closeTextContentBlock(). A similar consolidation would also benefit the tool content block closing logic.
Greptile SummaryThis PR fixes a "Content block not found" error that occurs when an upstream proxy (e.g. PackyCode) emits a text content block before the thinking block in extended thinking mode. The outbound transformer strips
Confidence Score: 5/5The change is safe to merge — it adds a well-scoped guard that mirrors an identical pattern used three other times in the same file. The fix is a minimal, targeted 20-line addition. It faithfully reproduces the text-block teardown logic already present in No files require special attention. Important Files Changed
Reviews (1): Last reviewed commit: "fix(llm): close text block before starti..." | Re-trigger Greptile |
|
有测试验证过其他渠道的影响吗,比如 deepseek |
目前测试下来只有opus4.7调用读类型的工具会出现问题,sonnet和deepseek都没问题 |
Summary
Root Cause
PackyCode proxy in extended thinking mode returns a short text block before the thinking block:
content_block_start index=0 type="text"
content_block_delta index=0 text_delta="\n"
content_block_stop index=0 ← text block closed normally
content_block_start index=1 type="thinking"
content_block_delta index=1 thinking_delta=...
The outbound transformer (Anthropic → LLM format) filters out
content_block_stopevents by design. So the LLM stream becomes:chunk: {Content: "\n"} ← from text_delta
chunk: {ReasoningContent: ...} ← from thinking_delta
The "close" signal for the text block is lost.
The inbound transformer (LLM → Anthropic format) receives
Content: "\n", setshasTextContentStarted = true, and emitscontent_block_start(index=0, text)+content_block_delta.Then when it receives
ReasoningContent, the code only checkedhasToolContentStarted(to close tool blocks) but did not checkhasTextContentStarted. It directly emittedcontent_block_start(index=0, thinking), reusing index 0 without closing the text block first.Claude Code sees two
content_block_startevents sharing index 0 with the first block never closed, and reports "Content block not found".Closes #1655