Skip to content

Fix Anthropic adapter streaming: reasoning_content thinking blocks + first-delta drop - #4

Merged
jwbron merged 1 commit into
mainfrom
anthropic-stream-reasoning-blocks
Jun 23, 2026
Merged

Fix Anthropic adapter streaming: reasoning_content thinking blocks + first-delta drop#4
jwbron merged 1 commit into
mainfrom
anthropic-stream-reasoning-blocks

Conversation

@jwbron

@jwbron jwbron commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Problem

Driving Claude Code at Kimi K2.7-Code through the proxy (cllm -m kimi-k2.7-code), the model's entire reasoning rendered in the console as the visible assistant text, and answers were missing or missing their first token ('m here. instead of I'm here.).

Root causes (two bugs in the OpenAI→Anthropic /v1/messages streaming adapter)

1. reasoning_content never opens a thinking block (transformation.py)

_translate_streaming_openai_chunk_to_anthropic_content_block only recognized Anthropic-native delta.thinking_blocks. OpenRouter reasoning models stream delta.reasoning → mapped to delta.reasoning_content, which fell through to the default ("text", TextBlock). Meanwhile the delta translator does map reasoning_contentthinking_delta. Net result: a content_block_start of type text receiving thinking_delta events — a malformed stream that Claude Code records as plain text, so the reasoning displays as the answer, gets fed back as assistant content on every subsequent turn, and progressively degrades the session.

Fix cherry-picked from upstream main: a reasoning_content branch that returns a thinking block start.

2. Block-type transitions drop the trigger chunk's delta (streaming_iterator.py)

On a transition the iterator queues content_block_stopcontent_block_start and re-queues the trigger chunk's delta only for input_json_delta. The first reasoning token of every thinking block and the first answer token after thinking were silently dropped; a one-chunk answer (391) vanished entirely. This one is still broken on upstream main.

Fix: also re-queue text_delta and thinking_delta trigger payloads — thinking_delta only when the new block start doesn't already embed the thinking content (it does for Anthropic-native thinking_blocks providers, which would otherwise duplicate it).

Validation

  • New tests: test_streaming_iterator_reasoning_blocks.py (sync + async reasoning→text streams reassemble to exactly thinking: 'We think.' / text: 'The answer.'; native thinking_blocks not duplicated).
  • All 92 tests in tests/test_litellm/llms/anthropic/experimental_pass_through/adapters/ pass.
  • Verified live against OpenRouter Kimi K2.7-Code: thinking block now complete from the first token, answer intact in its own text block; interactive Claude Code session confirmed fixed.

Once merged, bump LITELLM_FORK_COMMIT in clm-setup to the new merge commit.

…first-delta drop

Two fixes for the OpenAI->Anthropic /v1/messages streaming adapter, found
driving Claude Code at Kimi K2.7-Code via OpenRouter:

1. transformation.py: reasoning_content deltas (OpenRouter-style reasoning,
   no thinking_blocks) never matched a block type, so the stream opened a
   content_block_start of type "text" and then poured thinking_delta events
   into it - a malformed stream. Claude Code rendered the model's entire
   reasoning as the visible answer. Cherry-picked from upstream main: a
   reasoning_content branch that opens a proper thinking block.

2. streaming_iterator.py: on a block-type transition, the trigger chunk's
   delta was dropped unless it was an input_json_delta. That ate the first
   reasoning token of every thinking block and the first answer token after
   it (one-chunk answers vanished entirely). Re-queue text_delta and
   thinking_delta trigger payloads too; thinking_delta only when the block
   start doesn't already embed the content (it does for Anthropic-native
   thinking_blocks providers, which would otherwise duplicate).
jwbron added a commit to jwbron/egg that referenced this pull request Jun 13, 2026
)

* egg-litellm: port reasoning streaming fixes from jwbron/litellm#4

Add patches 4 and 5 to config/litellm/patch_litellm_cache.py:

- Patch 4: OpenRouter-style delta.reasoning_content now opens a proper
  Anthropic thinking content block instead of falling through to a text
  block. Without this, clients like Claude Code rendered the model's
  entire reasoning as visible assistant text and fed it back as assistant
  content on later turns.

- Patch 5: AnthropicStreamWrapper now re-queues the trigger chunk's first
  text_delta / thinking_delta on block-type transitions, not just
  input_json_delta tool transitions. This stops the first reasoning token
  and the first post-reasoning answer token from being silently dropped.
  Applied to both sync __next__ and async __anext__. Native
  thinking_blocks providers are guarded against duplication.

Update Dockerfile comment and patch labels/docstring to reflect five
patches.

Validated against a clean litellm==1.86.2 install: all patches apply,
idempotent, and a mocked reasoning stream reassembles to thinking:
'We think.' / text: 'The answer.' with first tokens intact.

Fixes #3190.

* egg-litellm: add patch-script tests + anchor Patch 4 needle

Address PR #3199 review (egg-reviewer, approve-with-suggestions):

- Anchor the Patch 4 needle on the preceding text-block elif
  (choice.delta.content ...) so it uniquely targets
  _translate_streaming_openai_chunk_to_anthropic_content_block and
  cannot silently retarget the sibling function on an upstream reorder.
- Refactor the patch specs into a module-level PATCHES list so tests can
  apply the exact needles to fixtures with no fixture/needle drift.
- Add tests/config/test_patch_litellm_cache.py: marker application,
  idempotency, fail-loud on drift/missing file, and a regression that
  locks in the Patch 4 needle-uniqueness anchor.

---------

Co-authored-by: egg-reviewer[bot] <261018737+egg-reviewer[bot]@users.noreply.github.com>
@jwbron
jwbron merged commit db6152a into main Jun 23, 2026
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.

1 participant