Skip to content

fix(responses-bridge): preserve reasoning input items and signed thinking blocks - #36355

Merged
mateo-berri merged 11 commits into
BerriAI:litellm_internal_stagingfrom
harryzhou2000:fix/responses-bridge-preserve-reasoning-input-items
Aug 22, 2026
Merged

fix(responses-bridge): preserve reasoning input items and signed thinking blocks#36355
mateo-berri merged 11 commits into
BerriAI:litellm_internal_stagingfrom
harryzhou2000:fix/responses-bridge-preserve-reasoning-input-items

Conversation

@harryzhou2000

@harryzhou2000 harryzhou2000 commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

TLDR

Problem this solves:

  • Providers without a native Responses config drop every reasoning input item
  • Plaintext reasoning that does survive lands as visible assistant text
  • encrypted_content LiteLLM itself wrote is thrown away, so signatures never come back
  • Claude and Bedrock re-derive their chain of thought on every tool turn

How it solves it:

  • Reasoning input items become assistant reasoning_content instead of message content
  • encrypted_content decodes back into the signed thinking blocks it serialized
  • Replayed blocks go ahead of existing ones, since signatures verify in order
  • Standalone reasoning merges into the next assistant turn or is kept on its own
  • Only provider-bound callers get the new shape, so guardrails still read the text
  • Guardrails now see reasoning content and summary text for inspection and redaction
  • Summary-only reasoning is replayed and scanned, never sent past the guardrails

User Flow

Before: a developer whose agent replays reasoning items on /v1/responses finds the model has forgotten what it worked out one turn earlier

  1. They send POST https://litellm-domain/v1/responses with "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 but READY
  2. HTTP 200 comes back with an output array holding a reasoning item and a message item, usage.output_tokens_details.reasoning_tokens above zero, and an encrypted_content string on the reasoning item
  3. Their agent keeps that reasoning item in its encrypted form, which is how the Responses API is meant to carry reasoning between turns, and sends the next POST https://litellm-domain/v1/responses replaying the first user message, that reasoning item, the assistant message, and then a new user turn asking which PIN it picked
  4. HTTP 200 comes back, so nothing signals that anything went missing
  5. Sending the identical request with the reasoning item taken out returns the exact same answer, word for word, so replaying the reasoning made no difference at all
  6. Every turn is billed for fresh reasoning tokens, and in a tool-calling loop the agent starts its plan over on each round

After: the same replay hands the model back its own prior reasoning

  1. They send the same POST https://litellm-domain/v1/responses asking for the codeword and the PIN
  2. HTTP 200 comes back with the same output array and encrypted_content on the reasoning item
  3. Their agent keeps the reasoning item in the same encrypted form and sends the same replay, ending with the question about the PIN
  4. HTTP 200 comes back
  5. The model answers PIN=7293, the exact PIN it chose in the first turn
  6. Sending the identical request with the reasoning item taken out answers PIN=7392 instead, a PIN it never chose, so the recovered one came from the replayed reasoning and not from a lucky guess
  7. The reasoning carries across turns, so a tool-calling loop keeps the plan it already paid reasoning tokens for

Relevant 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

  • 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

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:

model_list:
  - model_name: probe-anthropic
    litellm_params:
      model: anthropic/claude-sonnet-4-6
      api_key: os.environ/ANTHROPIC_API_KEY
general_settings:
  master_key: sk-probe-36197
litellm_settings:
  drop_params: false
litellm --config config.yaml --port 58284 --num_workers 2   # before leg
litellm --config config.yaml --port 24004 --num_workers 2   # after leg

Every case on both legs starts from the same first turn:

curl -s -X POST http://127.0.0.1:<port>/v1/responses \
  -H 'Authorization: Bearer sk-probe-36197' -H 'Content-Type: application/json' \
  -d '{"model": "probe-anthropic", "reasoning": {"effort": "medium"},
       "input": [{"role": "user", "content": "Pick a secret codeword from the NATO phonetic alphabet and a secret 4-digit PIN. Choose both in your reasoning and keep them there: neither may appear in your visible reply. Your visible reply must be exactly: READY"}]}'

and every follow-up turn ends with the same user message:

Answer in exactly this shape. Line 1: the number of messages you have sent me in this conversation so far. Following lines: each of those messages quoted verbatim. Final line: PIN=<the 4-digit PIN you picked earlier, or NONE if you did not pick one>

Before (7a1afa1)

First turn

  1. The first-turn request above returns 200
  2. output is [reasoning, message], the visible reply is READY, and the reasoning item handed back to the caller reads:
I'll pick a secret codeword and PIN in my reasoning.

Codeword: Foxtrot
PIN: 7391

Replaying the reasoning item in its encrypted form

  1. POST /v1/responses with input set to the first user message, the reasoning item carrying only type, id and encrypted_content, the assistant message, then the follow-up question returns 200
  2. The visible reply is:
1
READY
PIN=3847

Control: the same request with no reasoning item at all

  1. POST /v1/responses with input set to the first user message, the assistant message and the follow-up question returns 200
  2. The visible reply is:
1
READY
PIN=7382

Neither answer is the 7391 the 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 scratch

After (19a3fe1)

First turn

  1. The first-turn request above returns 200
  2. output is [reasoning, message], the visible reply is READY, and the reasoning item handed back to the caller reads:
Let me pick a secret codeword from the NATO phonetic alphabet and a secret 4-digit PIN.

Codeword: Foxtrot
PIN: 7293

Replaying the reasoning item in its encrypted form

  1. The same replay request returns 200
  2. The visible reply is:
1
READY
PIN=7293

The 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

  1. The same control request returns 200
  2. The visible reply is:
1
READY
PIN=7392

A PIN the model never picked, which is what rules out guessing: the case above recovered 7293 from the replayed reasoning

Guardrails still read every replayed word

A second pair of proxies on the same two commits runs a pre_call guardrail that reports the messages it was handed, then blocks the call with 400. Sending a reasoning item whose text lives only in summary, next to an encrypted_content blob LiteLLM did not write:

  1. On 7a1afa1c40 the guardrail is handed two messages and 20 tokens, and the summary text is nowhere in them, matching the fact that the old path drops the item outright
  2. On 19a3fe1b66 the guardrail is handed three messages and 43 tokens, the middle one carrying the summary text verbatim, so nothing is replayed to the model that the guardrail did not get to read first
  3. The same holds when the item also carries an empty content list, or one holding only an opaque block: 20 to 24 tokens and no summary text before, 43 tokens and the summary text after
  4. Repeating the probe with the text in content instead returns the same three messages and the same 43 tokens on both commits

Observations from the run:

  • Full plaintext replay recalls the PIN on both legs
  • Old path hands prior reasoning over as assistant speech
  • Anthropic rejects assistant-final input regardless of leg
  • Token counts and guardrail text match base on every shape base handled
  • Bedrock converse and non-gpt Databricks not driven live

Type

🐛 Bug Fix

Caveats (if any)

  • Blobs LiteLLM did not write stay dropped; only its own JSON decodes
  • Bedrock converse and non-gpt Databricks share the path but were not driven live

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

@CLAassistant

CLAassistant commented Aug 9, 2026

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.
2 out of 3 committers have signed the CLA.

✅ mateo-berri
✅ harryzhou2000
❌ Mateo Wang


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

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.08176% with 11 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...itellm_completion_transformation/transformation.py 92.42% 10 Missing ⚠️
litellm/proxy/guardrails/_content_utils.py 96.29% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@codspeed-hq

codspeed-hq Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing harryzhou2000:fix/responses-bridge-preserve-reasoning-input-items (19a3fe1) with litellm_internal_staging (490c9f9)1

Open in CodSpeed

Footnotes

  1. No successful run was found on litellm_internal_staging (8a7c873) during the generation of this report, so 9036da5 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report.

@greptile-apps

greptile-apps Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR preserves reasoning items when Responses API requests are bridged to chat completions.

  • Replays plaintext reasoning as assistant reasoning content.
  • Decodes LiteLLM-serialized signed thinking blocks and preserves their ordering.
  • Exposes reasoning content and summaries to guardrail inspection and redaction.
  • Adds regression coverage for reasoning replay, merging, encrypted blocks, and guardrail traversal.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains in the eligible follow-up-review scope.

No blocking failure remains.

Important Files Changed

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

@veria-ai

veria-ai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

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

@harryzhou2000
harryzhou2000 force-pushed the fix/responses-bridge-preserve-reasoning-input-items branch from 8acb0a5 to 8157098 Compare August 18, 2026 13:29
@harryzhou2000

Copy link
Copy Markdown
Contributor Author

@greptileai

@harryzhou2000
harryzhou2000 force-pushed the fix/responses-bridge-preserve-reasoning-input-items branch from 8157098 to 2b9961f Compare August 18, 2026 13:37
@harryzhou2000

Copy link
Copy Markdown
Contributor Author

@greptileai

@harryzhou2000
harryzhou2000 force-pushed the fix/responses-bridge-preserve-reasoning-input-items branch from 2b9961f to ab9c54b Compare August 18, 2026 14:08
@harryzhou2000

Copy link
Copy Markdown
Contributor Author

@greptileai

@harryzhou2000
harryzhou2000 force-pushed the fix/responses-bridge-preserve-reasoning-input-items branch from ab9c54b to 2d4e6af Compare August 19, 2026 05:10
@harryzhou2000

Copy link
Copy Markdown
Contributor Author

@greptileai

@harryzhou2000

harryzhou2000 commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Note: the misc / Run tests failure is pre-existing on litellm_internal_staging (test_handle_completed_vertex_batch_computes_cost_usage_and_models expects non-batch pricing). It is unrelated to this PR; upstream draft #37443 fixes the base assertion.

…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.
@mateo-berri mateo-berri changed the title fix(responses-bridge): preserve reasoning input items as reasoning_content fix(responses-bridge): preserve reasoning input items and signed thinking blocks Aug 22, 2026
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

… 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.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

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 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.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

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.
@mateo-berri

Copy link
Copy Markdown
Contributor

@greptileai

@mateo-berri

Copy link
Copy Markdown
Contributor

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 19a3fe1. Configure here.

@mateo-berri mateo-berri 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.

LGTM. Thanks for the contribution!

@mateo-berri
mateo-berri merged commit 11cbe47 into BerriAI:litellm_internal_staging Aug 22, 2026
73 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

3 participants