Skip to content

fix(llm): close text block before starting thinking block in inbound stream#1677

Merged
looplj merged 1 commit into
looplj:unstablefrom
hen7777777:fix/close-text-block-before-thinking
May 20, 2026
Merged

fix(llm): close text block before starting thinking block in inbound stream#1677
looplj merged 1 commit into
looplj:unstablefrom
hen7777777:fix/close-text-block-before-thinking

Conversation

@hen7777777

Copy link
Copy Markdown
Contributor

Summary

  • Fix "Content block not found" error in Claude Code when using Anthropic format channels with extended thinking enabled
  • The issue occurs when the upstream proxy (observed with PackyCode as the relay/proxy; unknown if official Anthropic API or other proxies exhibit the same behavior) returns a text content block before the thinking block (text → thinking order)
  • The inbound stream transformer now properly closes an open text block before starting a thinking block, mirroring the existing tool block handling

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_stop events 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", sets hasTextContentStarted = true, and emits content_block_start(index=0, text) + content_block_delta.

Then when it receives ReasoningContent, the code only checked hasToolContentStarted (to close tool blocks) but did not check hasTextContentStarted. It directly emitted content_block_start(index=0, thinking), reusing index 0 without closing the text block first.

Claude Code sees two content_block_start events sharing index 0 with the first block never closed, and reports "Content block not found".

Closes #1655

…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

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +332 to +349
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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-apps

greptile-apps Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This 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 content_block_stop events by design, so the inbound transformer was left seeing consecutive ReasoningContent after Content with no intervening close signal — causing index 0 to be reused for two different block types.

  • Adds a hasTextContentStarted guard in the ReasoningContent handler, mirroring the existing hasToolContentStarted guard: flushes pending citations, resets the flag, emits content_block_stop, and increments contentIndex before starting the thinking block.
  • The fix is minimal and consistent with the same pattern already applied in closeThinkingBlock, the redacted-thinking handler, and the tool-call handler.

Confidence Score: 5/5

The 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 closeThinkingBlock (lines 131–146) and in the redacted-thinking and tool-call handlers, so the behavior is well-established and easy to verify by comparison. No new state variables are introduced, contentIndex is incremented in the same way as all sibling paths, and flushPendingTextCitations is called before resetting the flag — consistent with every other call site in the file.

No files require special attention.

Important Files Changed

Filename Overview
llm/transformer/anthropic/inbound_stream.go Adds a missing hasTextContentStarted check in the ReasoningContent handler to properly close an open text block before emitting a thinking block start; the logic is consistent with existing patterns for tool blocks and the closeThinkingBlock helper.

Reviews (1): Last reviewed commit: "fix(llm): close text block before starti..." | Re-trigger Greptile

@looplj

looplj commented May 20, 2026

Copy link
Copy Markdown
Owner

有测试验证过其他渠道的影响吗,比如 deepseek

@hen7777777

Copy link
Copy Markdown
Contributor Author

有测试验证过其他渠道的影响吗,比如 deepseek

目前测试下来只有opus4.7调用读类型的工具会出现问题,sonnet和deepseek都没问题

@looplj looplj merged commit 729e285 into looplj:unstable May 20, 2026
4 checks passed
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/错误]: ClaudeCode 中使用 anphropic 格式报错 API Error: Content block not found

2 participants