Skip to content

fix(anthropic): round-trip thinking blocks to OpenAI backends on /v1/messages - #37953

Merged
mateo-berri merged 8 commits into
litellm_internal_stagingfrom
litellm_fix_24985_thinking_roundtrip
Aug 24, 2026
Merged

fix(anthropic): round-trip thinking blocks to OpenAI backends on /v1/messages#37953
mateo-berri merged 8 commits into
litellm_internal_stagingfrom
litellm_fix_24985_thinking_roundtrip

Conversation

@mateo-berri

@mateo-berri mateo-berri commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • /v1/messages drops prior-turn thinking on OpenAI-family backends
  • The Responses path flattens thinking into visible assistant text
  • The Chat Completions path sends thinking_blocks but never reasoning_content
  • Moonshot models refuse the turn, or answer without the reasoning
  • With top-level thinking, gpt-5 loses the whole assistant turn

How it solves it:

  • Assistant thinking blocks now become a Responses reasoning input item
  • Reasoning items come back as thinking blocks instead of visible prose
  • The Chat Completions path sets reasoning_content from the thinking blocks
  • The completion-to-responses bridge keeps assistant content and reasoning

User Flow

Before: a Claude Code user on an OpenAI-family model behind LiteLLM has last turn's reasoning thrown away, so the follow-up is either refused or answered blind

  1. They set ANTHROPIC_BASE_URL to https://litellm-domain and start Claude Code on a reasoning model
  2. They ask something that needs a tool, so Claude Code sends POST https://litellm-domain/v1/messages with "thinking": {"type": "enabled", "budget_tokens": 4000}
  3. A 200 comes back holding a thinking block, a text block, and a tool_use block, and Claude Code prints the thinking on screen
  4. Claude Code runs the tool and sends the follow-up POST https://litellm-domain/v1/messages, replaying that whole assistant turn plus a tool_result
  5. On a Moonshot model the follow-up is refused with 400 thinking is enabled but reasoning_content is missing; on newer Moonshot models a 200 comes back whose answer starts the reasoning over from nothing
  6. On an OpenAI model the 200 comes back, but the private scratchpad from step 3 was handed back to the model as its own visible reply, so it answers as if it had said all of that out loud to the user
  7. On a gpt-5 model the 200 answers as though step 3 never happened at all: the reasoning, the reply, and the tool call are all missing from what the model sees

After: the same session carries last turn's reasoning across, so every follow-up is accepted and answered in context

  1. They set ANTHROPIC_BASE_URL to https://litellm-domain and start Claude Code on a reasoning model
  2. They ask something that needs a tool, so Claude Code sends POST https://litellm-domain/v1/messages with "thinking": {"type": "enabled", "budget_tokens": 4000}
  3. A 200 comes back holding a thinking block, a text block, and a tool_use block, and Claude Code prints the thinking on screen
  4. Claude Code runs the tool and sends the follow-up POST https://litellm-domain/v1/messages, replaying that whole assistant turn plus a tool_result
  5. On a Moonshot model the follow-up is accepted with a 200 and the answer builds on the reasoning from step 3
  6. On an OpenAI model the reasoning travels as reasoning, so the only words handed back to the model as its own reply are the ones the user actually saw
  7. On a gpt-5 model the follow-up sees the whole previous turn: its reasoning, its reply, and its tool call

Relevant issues

Fixes #24985

Linear ticket

Resolves LIT-6006

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • I have added meaningful tests
  • The handful of test files covering my change pass locally, e.g. uv run pytest tests/test_litellm/<your_test_file>.py -v. Leave the suites (make test-unit-*, make test-unit) to CI: it finishes in ~15 minutes where a laptop takes an hour or more
  • My PR passes all required CI/CD checks (e.g., lint, schema.d.ts sync check, etc.)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Shared setup, identical on both sides. Two proxies, one per adapter path, each with a callback that writes the exact upstream request body to a file

config_responses.yaml (the default path for OpenAI):

model_list:
  - model_name: gpt5-resp
    litellm_params:
      model: openai/gpt-5.1
      api_key: os.environ/OPENAI_API_KEY
litellm_settings:
  reasoning_auto_summary: true
  callbacks: ["raw_capture.raw_capture"]
general_settings:
  master_key: sk-qa6006

config_chat.yaml (the opt-in path):

model_list:
  - model_name: kimi
    litellm_params:
      model: moonshot/kimi-k2.6
      api_key: os.environ/MOONSHOT_API_KEY
  - model_name: gpt5-chat
    litellm_params:
      model: openai/gpt-5.1
      api_key: os.environ/OPENAI_API_KEY
litellm_settings:
  drop_params: true
  reasoning_auto_summary: true
  use_chat_completions_url_for_anthropic_messages: true
  callbacks: ["raw_capture.raw_capture"]
general_settings:
  master_key: sk-qa6006

Every case runs the same two real turns against the real provider, the way Claude Code does:

# turn 1: a question that needs a tool, thinking enabled
curl -sS -X POST "http://127.0.0.1:$PORT/v1/messages" \
  -H 'content-type: application/json' -H 'x-api-key: sk-qa6006' \
  -H 'anthropic-version: 2023-06-01' \
  -d '{"model":"'"$MODEL"'","max_tokens":6000,
       "thinking":{"type":"enabled","budget_tokens":4000},
       "tools":[{"name":"get_weather","description":"Get the current weather in a city.",
                 "input_schema":{"type":"object","properties":{"city":{"type":"string"}},"required":["city"]}}],
       "messages":[{"role":"user","content":"I am planning a picnic. Carefully reason about which of San Francisco, Denver, or Miami would be best given it is late August, considering fog patterns, altitude effects on UV, and humidity. Then call get_weather for whichever city you decide is most promising. Think it through step by step before calling the tool."}]}' \
  > turn1_resp.json

# turn 2: replay turn 1's assistant content verbatim, add the tool_result
jq -s '...' turn1_resp.json > turn2_req.json   # builds messages: [user, assistant=turn1 content, user=tool_result]
curl -sS -X POST "http://127.0.0.1:$PORT/v1/messages" \
  -H 'content-type: application/json' -H 'x-api-key: sk-qa6006' \
  -H 'anthropic-version: 2023-06-01' -d @turn2_req.json > turn2_resp.json

Case 5 swaps curl for the real client: Claude Code 2.1.241 running interactively in tmux, keystrokes sent with tmux send-keys and the screen read back with tmux capture-pane, pointed at the proxy with ANTHROPIC_BASE_URL=http://127.0.0.1:$PORT, ANTHROPIC_AUTH_TOKEN=sk-qa6006, and ANTHROPIC_MODEL set to the proxy's model name. Two prompts typed in order, so the second request replays the first turn's thinking block exactly as Claude Code builds it:

Without using any tools or reading any files, think it through: for a late-August picnic,
which of San Francisco, Denver, or Miami is best, considering fog, altitude UV, and humidity?
Answer in one sentence.

Still without tools, think again: of the two cities you rejected, which is the runner-up and why? One sentence.

Before (7a1afa1)

Case 1: OpenAI on the default Responses path

  1. Responses proxy, MODEL=gpt5-resp, run the two turns above
  2. Turn 1 comes back with thinking on screen:
    TURN 1 content block types: ["thinking","thinking","text","tool_use"]
    
  3. Read the body the proxy actually sent upstream on turn 2. The model's two private scratchpads are quoted back to it as its own visible reply, and no reasoning item is sent at all:
    upstream: POST /v1/responses
    0 message user      [('input_text', 'I am planning a picnic. Carefully reason about which of San ')]
    1 function_call    {"call_id": "call_2vHkH9SyRZgMRbSOudBlj4gc", "name": "get_weather", ...}
    2 message assistant [('output_text', '**Choosing the best city for a picnic**\n\nI need to consider '),
                         ('output_text', '**Deciding on a picnic location**\n\nFor a picnic, I’d likely '),
                         ('output_text', 'I’ll compare the three cities on typical late‑August conditi')]
    3 function_call_output {"call_id": "call_2vHkH9SyRZgMRbSOudBlj4gc", ...}
    reasoning items: 0
    

Case 2: Moonshot on the Chat Completions path

  1. Chat proxy, MODEL=kimi, run the two turns above
  2. Turn 1 comes back with thinking on screen:
    TURN 1 content block types: ["thinking","text","tool_use"]
    
  3. Read the upstream body on turn 2. The thinking blocks ride along, but reasoning_content is a single space, so the model is told it reasoned about nothing:
    upstream: POST /chat/completions
    0 user      keys=['content', 'role']
    1 assistant keys=['content', 'reasoning_content', 'role', 'thinking_blocks', 'tool_calls']
        reasoning_content: " "
        thinking_blocks:   [('thinking', 'The user wants me to reason about which of three cities (San')]
    2 tool      keys=['content', 'role', 'tool_call_id']
    

Case 3: gpt-5 on the Chat Completions path, re-bridged to /v1/responses

  1. Chat proxy, MODEL=gpt5-chat, run the two turns above
  2. Turn 1 comes back with thinking on screen:
    TURN 1 content block types: ["thinking","text","tool_use"]
    
  3. Read the upstream body on turn 2. The entire assistant turn is gone: no reasoning, no reply, only the bare tool call and its result:
    upstream: POST /v1/responses
    0 message user      [('input_text', 'I am planning a picnic. Carefully reason about which of San ')]
    1 function_call    {"call_id": "call_wbqZ9To0tBVTnf3QTMs6btxa", "name": "get_weather", ...}
    2 function_call_output {"call_id": "call_wbqZ9To0tBVTnf3QTMs6btxa", ...}
    reasoning items: 0
    

Case 4: streaming on the Responses path, the shape Claude Code actually sends

  1. Responses proxy, MODEL=gpt5-resp, same two turns with "stream": true, reassembling turn 1's blocks from the SSE events
  2. The thinking block is opened, filled, and closed with no signature ever streamed:
    TURN 1 stream event sequence: message_start content_block_start thinking_deltax95 content_block_stop
      content_block_start text_deltax49 content_block_stop
      content_block_start input_json_deltax6 content_block_stop message_delta message_stop
    TURN 1 thinking signatures: [None]
    
  3. Read the upstream body on turn 2. Same flattening as case 1, so the reasoning goes up as visible assistant prose:
    upstream: POST /v1/responses
    0 message user      [('input_text', 'I am planning a picnic. Carefully reason about which of San ')]
    1 function_call    {"call_id": "call_MLfFktPUOHoJ0oHHZrl6PjIh", "name": "get_weather", ...}
    2 message assistant [('output_text', "**Choosing weather options**\n\nI'm considering Denver, where "),
                         ('output_text', 'I’ll evaluate San Francisco, Denver, and Miami for a late‑Au')]
    3 function_call_output {"call_id": "call_MLfFktPUOHoJ0oHHZrl6PjIh", ...}
    reasoning items: 0
    

Case 5: real Claude Code, driven interactively

  1. Proxies for this leg booted from the current merge base 11cbe47; the other Before cases ran at 7a1afa1, and no commit between the two touches these paths. Responses proxy on gpt5-resp, Chat proxy on kimi
  2. Both sessions think and answer on screen both turns. The Responses pane:
    ❯ Still without tools, think again: of the two cities you rejected, which is the runner-up and why? One sentence.
      Thought for 19s (ctrl+o to expand)
    ⏺ San Francisco is the runner-up because its cooler, drier air and sea-level UV are easier to manage than
      Miami's oppressive humidity and frequent late-summer storms
    
    and the Chat pane:
    ❯ Still without tools, think again: of the two cities you rejected, which is the runner-up and why? One sentence.
      Thought for 2m 40s (ctrl+o to expand)
    ⏺ San Francisco is runner-up because fog beats Miami's swampy late-August humidity
    
  3. Upstream body on the second turn, Responses path: the first turn's thinking is quoted back as the model's own visible reply, and no reasoning item goes up
    upstream: /v1/responses, model: gpt-5.1
    2 message assistant [('output_text', '**Evaluating picnic options**\n\nI need to decide the best cit'),
                         ('output_text', 'Denver is best because late‑August is sunny and dry, avoidin')]
    reasoning items: 0
    
  4. Upstream body on the second turn, Chat path: the thinking block rides along but no reasoning_content is sent at all (with a tool call in the turn, this same path sends the " " placeholder from case 2)
    upstream: chat/completions, model: kimi-k2.6
    3 assistant keys=['content', 'role', 'thinking_blocks']
        thinking_blocks:   [('thinking', 'The user wants a one-sentence answer about which city (San F')]
    

After (e02f34b)

Case 1: OpenAI on the default Responses path

  1. Responses proxy, MODEL=gpt5-resp, run the same two turns
  2. Turn 1 comes back with thinking on screen:
    TURN 1 content block types: ["thinking","tool_use"]
    
  3. Read the upstream body on turn 2. The reasoning now travels as a reasoning item instead of being quoted back as the model's own visible reply:
    upstream: POST /v1/responses
    0 message user      [('input_text', 'I am planning a picnic. Carefully reason about which of San ')]
    1 reasoning        id=None summary=['**Choosing the right picnic location**\n\nI need to consider t']
    2 function_call    {"call_id": "call_6FWKmRyq7Oz5CmugOk7v9BLw", "name": "get_weather", ...}
    3 function_call_output {"call_id": "call_6FWKmRyq7Oz5CmugOk7v9BLw", ...}
    reasoning items: 1
    
  4. Turn 2 is accepted and answers in context:
    TURN 2 content block types: ["text"]
    TURN 2 error (if any):
    

Case 2: Moonshot on the Chat Completions path

  1. Chat proxy, MODEL=kimi, run the same two turns
  2. Turn 1 comes back with thinking on screen:
    TURN 1 content block types: ["thinking","text","tool_use"]
    
  3. Read the upstream body on turn 2. reasoning_content now carries the real prior reasoning instead of a placeholder:
    upstream: POST /chat/completions
    0 user      keys=['content', 'role']
    1 assistant keys=['content', 'reasoning_content', 'role', 'thinking_blocks', 'tool_calls']
        reasoning_content: "The user wants me to reason about which of three cities (San Francisco, Denver,
                            or Miami) would be best for a picnic in late August, considering:
                            1. Fog patterns 2. Altitude effects on UV 3. Humidity..."
        thinking_blocks:   [('thinking', 'The user wants me to reason about which of three cities (San')]
    2 tool      keys=['content', 'role', 'tool_call_id']
    
  4. Turn 2 is accepted and keeps reasoning:
    TURN 2 content block types: ["thinking","text"]
    TURN 2 error (if any):
    

Case 3: gpt-5 on the Chat Completions path, re-bridged to /v1/responses

  1. Chat proxy, MODEL=gpt5-chat, run the same two turns
  2. Turn 1 comes back with thinking on screen:
    TURN 1 content block types: ["thinking","tool_use"]
    
  3. Read the upstream body on turn 2. The assistant turn survives now, where the Before run dropped it whole:
    upstream: POST /v1/responses
    0 message user      [('input_text', 'I am planning a picnic. Carefully reason about which of San ')]
    1 reasoning        id=None summary=['**Considering picnic locations**\n\nI need to think about the ']
    2 function_call    {"call_id": "call_erasAAfyytE0ih45IjRpe8zx", "name": "get_weather", ...}
    3 function_call_output {"call_id": "call_erasAAfyytE0ih45IjRpe8zx", ...}
    reasoning items: 1
    
  4. Turn 2 is accepted and keeps reasoning:
    TURN 2 content block types: ["thinking","text"]
    TURN 2 error (if any):
    

Case 4: streaming on the Responses path, the shape Claude Code actually sends

  1. Responses proxy, MODEL=gpt5-resp, same two turns with "stream": true, reassembling turn 1's blocks from the SSE events
  2. The stream still carries no signature, matching the Before run, because only Anthropic can sign a thinking block:
    TURN 1 stream event sequence: message_start content_block_start thinking_deltax209 content_block_stop
      content_block_start text_deltax232 content_block_stop
      content_block_start input_json_deltax5 content_block_stop message_delta message_stop
    TURN 1 thinking signatures: ['']
    
  3. Read the upstream body on turn 2. The reassembled thinking block goes back up as a reasoning item, not as prose, and the assistant reply survives alongside it:
    upstream: POST /v1/responses
    0 message user      [('input_text', 'I am planning a picnic. Carefully reason about which of San ')]
    1 reasoning        id=None summary=['**Considering ideal picnic weather**\n\nI want to give a brief']
    2 function_call    {"call_id": "call_fDiClZ5uR41iQQzZ3meYJJce", "name": "get_weather", ...}
    3 message assistant [('output_text', 'Denver is likely the most promising of the three for a late‑')]
    4 function_call_output {"call_id": "call_fDiClZ5uR41iQQzZ3meYJJce", ...}
    reasoning items: 1
    
  4. Turn 2 streams back cleanly:
    TURN 2 block types: ['text']
    TURN 2 stream errors: none
    

Case 5: real Claude Code, driven interactively

  1. Same two sessions, proxies booted from e02f34b
  2. Both sessions still think and answer on screen both turns. The Responses pane:
    ❯ Still without tools, think again: of the two cities you rejected, which is the runner-up and why? One sentence.
      Thought for 14s (ctrl+o to expand)
    ⏺ San Francisco is the runner-up because cooler temperatures and moderate humidity beat Miami's oppressive
      heat, humidity, and intense late-summer UV
    
    and the Chat pane:
    ❯ Still without tools, think again: of the two cities you rejected, which is the runner-up and why? One sentence.
      Thought for 1m 46s (ctrl+o to expand)
    ⏺ San Francisco is runner-up because cool fog beats Miami's sweltering humidity
    
  3. Upstream body on the second turn, Responses path: the thinking travels as a reasoning item and the only assistant text is the reply the user actually saw
    upstream: /v1/responses, model: gpt-5.1
    2 reasoning id=<absent> summary=[('summary_text', '**Considering picnic locations**\n\nThe user wants a one-sente')]
    3 message assistant [('output_text', 'Denver is best for a late-August picnic since it is sunny an')]
    reasoning items: 1
    
  4. Upstream body on the second turn, Chat path: reasoning_content now carries the real prior reasoning
    upstream: chat/completions, model: kimi-k2.6
    3 assistant keys=['content', 'reasoning_content', 'role', 'thinking_blocks']
        reasoning_content: 'The user wants a one-sentence answer about which city (San Francisco, Denver, or'
        thinking_blocks:   [('thinking', 'The user wants a one-sentence answer about which city (San F')]
    

Type

🐛 Bug Fix

Caveats (if any)

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR
  • e02f34b passes /live-pr-risk

Note

Medium Risk
Touches multi-provider message transformation for reasoning/tool history, which can change what models see on follow-up turns. No auth or data-store changes; risk is incorrect request shaping if a conversion edge case is missed.

Overview
Preserves prior-turn Anthropic thinking when /v1/messages is proxied to OpenAI-family backends, so follow-ups keep reasoning instead of dropping it, flattening it into visible assistant text, or losing the whole assistant turn (including tool calls).

Responses path: assistant thinking blocks become Responses reasoning items (no fabricated id). Consecutive thinking blocks collapse into one item; a tool call splits them. Responses reasoning maps back to unsigned thinking blocks, not output_text.

Chat Completions path: thinking text is also copied into reasoning_content so models like Moonshot/DeepSeek do not get a blank placeholder. The chat→Responses bridge now keeps assistant text next to tool calls and still emits reasoning for thinking-only turns. Stored reasoning_items win over re-derived thinking.

Azure AI, Fireworks, and hosted vLLM now strip reasoning_content the same way they already strip thinking_blocks. Streaming thinking blocks open with an empty signature and never stream a stand-in signature.

Reviewed by Cursor Bugbot for commit e02f34b. Bugbot is set up for automated code reviews on this repo. Configure here.

The experimental /v1/messages adapters lost prior-turn reasoning three
different ways once the request left for an OpenAI-shaped backend.

On the Responses path, thinking blocks were flattened into output_text
inside the assistant message, so the model read its own private reasoning
back as visible prose and no reasoning item was ever sent. They now become
Responses reasoning input items, grouped by signature so summary parts that
arrived as one item go back as one item. The response direction stops
hardcoding signature=None and carries the reasoning item id, which is what
lets the next turn regroup them; the streaming wrapper emits the matching
signature_delta.

On the chat completions path the adapter attached thinking_blocks but never
set reasoning_content, so Moonshot and DeepSeek substituted a single-space
placeholder and other providers sent nothing. It is now derived from the
thinking blocks.

With use_chat_completions_url_for_anthropic_messages and a model that itself
bridges to /v1/responses, the assistant message was dropped whole: reasoning,
text, and all. That branch now emits the reasoning items and the message
content alongside the tool calls.

Fixes #24985
@greptile-apps

greptile-apps Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR preserves Anthropic thinking blocks when routing /v1/messages conversations through OpenAI-family backends.

  • Converts replayed thinking blocks into Responses API reasoning items.
  • Populates Chat Completions reasoning_content from readable thinking blocks.
  • Preserves assistant text alongside tool calls through the completion-to-responses bridge.
  • Converts response reasoning items back into unsigned Anthropic thinking blocks and strips unsupported reasoning fields for selected providers.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py Adds bidirectional conversion between Anthropic thinking blocks and Responses reasoning items while preserving visible assistant text and tool calls.
litellm/litellm_core_utils/prompt_templates/common_utils.py Adds shared helpers for extracting readable thinking text and constructing chat or Responses reasoning payloads.
litellm/completion_extras/litellm_responses_transformation/transformation.py Preserves assistant reasoning and visible content when bridging Chat Completions messages into Responses input.
litellm/llms/anthropic/experimental_pass_through/adapters/transformation.py Populates reasoning_content when translating Anthropic assistant thinking blocks to chat messages.
litellm/llms/anthropic/experimental_pass_through/responses_adapters/streaming_iterator.py Emits streamed thinking blocks with an empty signature instead of fabricating a provider signature.
litellm/llms/azure_ai/chat/transformation.py Strips unsupported reasoning_content from Azure AI Foundry messages.
litellm/llms/fireworks_ai/chat/transformation.py Strips reasoning_content alongside other unsupported assistant fields.
litellm/llms/hosted_vllm/chat/transformation.py Removes reasoning_content from assistant messages before sending them to hosted vLLM.

Reviews (5): Last reviewed commit: "Merge remote-tracking branch 'origin/lit..." | Re-trigger Greptile

@codecov

codecov Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing litellm_fix_24985_thinking_roundtrip (e02f34b) with litellm_internal_staging (aae36f4)

Open in CodSpeed

…ning_content

A reasoning item id is not an Anthropic signature. Passing it off as one got the
block replayed to Anthropic and Bedrock as if it were real, and every backend that
verifies signatures rejected the turn. Thinking blocks now come back unsigned, and
the streaming path no longer emits a signature_delta for them.

Azure AI Foundry, Fireworks, and vLLM reject unknown message fields, so they now
strip reasoning_content alongside thinking_blocks the way Mistral already did.

The thinking-block helpers take ChatCompletionThinkingBlock and
ChatCompletionRedactedThinkingBlock instead of loose mappings.
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 32bf1ab. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 5317a5a. Configure here.

@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for both issues found in the latest run.

  • ✅ Fixed: Null summary becomes literal None
    • Changed _summary_part_text to coerce a null text value to an empty string via or "" so callers correctly skip it instead of emitting a thinking block with the literal string "None".
  • ✅ Fixed: Thinking-only turns drop reasoning
    • Added a fallback elif role == "assistant" branch that emits _reasoning_input_items(msg) when the assistant turn has no content and no tool calls so prior-turn reasoning survives the chat-to-Responses bridge.

Create PR

You can send follow-ups to the cloud agent here.

@CLAassistant

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
1 out of 2 committers have signed the CLA.

✅ mateo-berri
❌ cursoragent
You have signed the CLA already but the status is still pending? Let us recheck it.

assistant_message["thinking_blocks"] = thinking_blocks
reasoning_content = reasoning_content_from_thinking_blocks(thinking_blocks)
if reasoning_content:
assistant_message["reasoning_content"] = reasoning_content

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.

Low: Thinking blocks bypass input guardrails

A user can place content in an assistant thinking block that the Anthropic guardrail translation does not add to its scanned texts, then have this code forward it to reasoning-capable backends as reasoning_content. Include readable thinking-block text in the pre-call guardrail extraction and write any masked result back before promoting it here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"bypass input guardrails"

Pre-existing surface: these blocks already went upstream via thinking_blocks and as flattened output_text. Scanning thinking text is a guardrail-layer follow-up.

@veria-ai

veria-ai Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

PR overview

This pull request updates the Anthropic /v1/messages adapter to preserve assistant thinking blocks when requests are translated and forwarded to OpenAI reasoning-capable backends.

One security issue remains open: readable content in thinking blocks can be forwarded as reasoning content without being included in pre-call guardrail scanning. This permits guardrail evasion, although the resulting impact depends on the configured guardrails and downstream backend behavior. No issues have yet been addressed.

Open issues (1)

Fixed/addressed: 0 · PR risk: 4/10

…trip' into litellm_fix_24985_thinking_roundtrip

# Conflicts:
#	litellm/llms/anthropic/experimental_pass_through/responses_adapters/transformation.py
@mateo-berri

Copy link
Copy Markdown
Contributor Author

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor Author

bugbot run

@cursor cursor 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e02f34b. Configure here.

@mateo-berri
mateo-berri enabled auto-merge August 24, 2026 17:57
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]: Anthropic-to-OpenAI adapter does not round-trip thinking blocks in multi-turn conversations

4 participants