fix(responses-bridge): preserve reasoning input items and signed thinking blocks - #36355
Conversation
|
Mateo Wang seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account. You have signed the CLA already but the status is still pending? Let us recheck it. |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Greptile SummaryThe PR preserves reasoning items when Responses API requests are bridged to chat completions.
Confidence Score: 5/5The PR appears safe to merge because no blocking failure remains in the eligible follow-up-review scope. No blocking failure remains.
|
| Filename | Overview |
|---|---|
| litellm/responses/litellm_completion_transformation/transformation.py | Adds provider-bound reasoning replay, signed thinking-block decoding, and merging of standalone reasoning into assistant turns. |
| litellm/responses/litellm_completion_transformation/session_handler.py | Enables reasoning replay when reconstructing provider-bound session messages. |
| litellm/proxy/guardrails/_content_utils.py | Extends guardrail inspection and rewriting to reasoning content and summary text. |
| tests/test_litellm/responses/litellm_completion_transformation/test_reasoning_input_item_preservation.py | Covers plaintext and signed reasoning replay, assistant-message merging, and inspection-path behavior. |
| tests/test_litellm/proxy/guardrails/test_content_utils.py | Verifies reasoning content and summaries are inspected and redacted. |
Reviews (9): Last reviewed commit: "fix(responses-bridge): fall back to summ..." | Re-trigger Greptile
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 1 · PR risk: 0/10 |
8acb0a5 to
8157098
Compare
8157098 to
2b9961f
Compare
2b9961f to
ab9c54b
Compare
…already has reasoning_content
ab9c54b to
2d4e6af
Compare
|
Note: the |
…itellm_lit6007_pr36355
…bridge encrypted_content on a reasoning input item is written by LiteLLM's own _encode_thinking_blocks as a JSON array of Anthropic/Bedrock thinking blocks, so decode it back and replay the signed blocks on the assistant message instead of dropping them. Providers without a native ResponsesAPIConfig now keep the verifiable chain-of-thought across turns, and prior-turn reasoning stops reaching the provider as visible assistant text.
… callers Guardrails, token counting and rate limiting share the input transform with the provider path, so moving reasoning onto reasoning_content hid it from them. Provider-bound callers opt in with replay_reasoning.
|
bugbot run |
There was a problem hiding this comment.
✅ 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 ae25da3. Configure here.
A reasoning input item that carries only summary text is replayed to the provider as reasoning_content, so inspection-only callers must see that text too. They used to fall through to the generic content branch, which reads content and drops a summary-only item, leaving guardrails and token counters blind to text the model still receives.
|
bugbot run |
… none An empty content list, or one holding only opaque blocks, still lets the provider-bound branch replay the summary text. The inspection path treated any non-None content as final, so that replayed text stayed invisible to guardrails and token counting.
|
bugbot run |
There was a problem hiding this comment.
✅ 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 19a3fe1. Configure here.
mateo-berri
left a comment
There was a problem hiding this comment.
LGTM. Thanks for the contribution!
11cbe47
into
BerriAI:litellm_internal_staging
TLDR
Problem this solves:
reasoninginput itemencrypted_contentLiteLLM itself wrote is thrown away, so signatures never come backHow it solves it:
reasoning_contentinstead of message contentencrypted_contentdecodes back into the signed thinking blocks it serializedUser Flow
Before: a developer whose agent replays reasoning items on
/v1/responsesfinds the model has forgotten what it worked out one turn earlierPOST https://litellm-domain/v1/responseswith"model": "claude-sonnet-4-6"and"reasoning": {"effort": "medium"}, asking the model to settle a secret codeword and a 4-digit PIN in its reasoning and reply with nothing butREADYoutputarray holding areasoningitem and amessageitem,usage.output_tokens_details.reasoning_tokensabove zero, and anencrypted_contentstring on the reasoning itemPOST https://litellm-domain/v1/responsesreplaying the first user message, that reasoning item, the assistant message, and then a new user turn asking which PIN it pickedAfter: the same replay hands the model back its own prior reasoning
POST https://litellm-domain/v1/responsesasking for the codeword and the PINoutputarray andencrypted_contenton the reasoning itemPIN=7293, the exact PIN it chose in the first turnPIN=7392instead, a PIN it never chose, so the recovered one came from the replayed reasoning and not from a lucky guessRelevant issues
Fixes #36197
Related: #27630, a drop-based alternative for the same input item
Linear ticket
Resolves LIT-6007
Pre-Submission checklist
Please complete all items before asking a LiteLLM maintainer to review your PR
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@greptileaito 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
Live against the real Anthropic API, no mocks, real spend. Two proxies, one at the merge base and one at this PR's tip, each booted with two uvicorn workers on its own random high port, sharing one config:
Every case on both legs starts from the same first turn:
and every follow-up turn ends with the same user message:
Before (7a1afa1)
First turn
200outputis[reasoning, message], the visible reply isREADY, and the reasoning item handed back to the caller reads:Replaying the reasoning item in its encrypted form
POST /v1/responseswithinputset to the first user message, the reasoning item carrying onlytype,idandencrypted_content, the assistant message, then the follow-up question returns200Control: the same request with no reasoning item at all
POST /v1/responseswithinputset to the first user message, the assistant message and the follow-up question returns200Neither answer is the
7391the model actually picked, and replaying the reasoning lands no closer than dropping it: the item never reaches the model, so both cases are the model guessing from scratchAfter (19a3fe1)
First turn
200outputis[reasoning, message], the visible reply isREADY, and the reasoning item handed back to the caller reads:Replaying the reasoning item in its encrypted form
200The PIN matches the one chosen in the first turn, so the reasoning survived the turn
Control: the same request with no reasoning item at all
200A PIN the model never picked, which is what rules out guessing: the case above recovered
7293from the replayed reasoningGuardrails still read every replayed word
A second pair of proxies on the same two commits runs a
pre_callguardrail that reports the messages it was handed, then blocks the call with400. Sending a reasoning item whose text lives only insummary, next to anencrypted_contentblob LiteLLM did not write:7a1afa1c40the guardrail is handed two messages and20tokens, and the summary text is nowhere in them, matching the fact that the old path drops the item outright19a3fe1b66the guardrail is handed three messages and43tokens, the middle one carrying the summary text verbatim, so nothing is replayed to the model that the guardrail did not get to read firstcontentlist, or one holding only an opaque block:20to24tokens and no summary text before,43tokens and the summary text aftercontentinstead returns the same three messages and the same43tokens on both commitsObservations from the run:
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
19a3fe1 passes /live-pr-risk