Skip to content

fix: stop feeding prior-turn reasoning back into the Responses gateway tool loop#122

Open
ashwing wants to merge 1 commit into
vllm-project:mainfrom
ashwing:fix/messages-reasoning-carryforward
Open

fix: stop feeding prior-turn reasoning back into the Responses gateway tool loop#122
ashwing wants to merge 1 commit into
vllm-project:mainfrom
ashwing:fix/messages-reasoning-carryforward

Conversation

@ashwing

@ashwing ashwing commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Summary

Fixes #120.

In a multi-round gateway tool loop (/v1/responses, HTTP or WebSocket), raw <think>...</think> reasoning markup leaks into the client's visible output_text channel — reasoning also streams correctly on its own reasoning_text channel, so it effectively appears twice, once as markup inside the answer.

This affects any client driving the Responses gateway loop with gateway-owned tools (web_search / MCP) over more than one round — Codex, or any /v1/responses caller. It's not client-specific; the trigger is the model's reasoning parser, not the caller. (Note: the /v1/messages path is currently a transparent proxy and does not go through this loop, so it is not affected.)

Root cause: the loop's Continue branch (executor/engine.rs) calls append_output_items_to_input(&mut ctx.enriched_request.input, &current_output), which fed each round's reasoning item back into the next inference round (OutputItem::to_input_item maps Reasoning → InputItem::Reasoning). Reasoning parsers (e.g. Qwen3) then re-emit that prior-turn reasoning wrapped in <think> on the visible text channel.

Reasoning is ephemeral to the turn that produced it — there's no reason to feed it back into subsequent inference rounds. This scopes the fix to the loop carry-forward: append_output_items_to_input now skips Reasoning, carrying forward only messages and tool calls.

Deliberately scoped: to_input_item is also used by the previous_response_id rehydration path (storage/types/item.rs::into_input_items), where reasoning should be preserved. That path is untouched — its test_into_input_items_includes_reasoning still passes. The client-visible/persisted output path (public_output_itemspayload.output) is also untouched, so reasoning is still shown to the client; only what's fed back into inference changes.

Test Plan

  • New unit test carry_forward_drops_reasoning_keeps_messages_and_calls: feeds reasoning + message + function_call through append_output_items_to_input and asserts reasoning is dropped while message/function_call carry forward. Verified it fails on the pre-fix code and passes with the fix.
  • cargo test -p agentic-server-core — 174/174 lib tests pass, including test_into_input_items_includes_reasoning (rehydration still preserves reasoning — no regression).
  • cargo fmt -- --check clean; cargo clippy -p agentic-server-core --all-targets -- -D warnings clean.
  • Root cause localized live (gateway → vLLM Qwen3 on GPU) with layered vLLM-direct tests: vLLM is clean unless a prior reasoning item is present in the input history (single-turn: clean; round-2 with function_call + output but no reasoning: clean; round-2 with reasoning fed back: leaks; round-2 with reasoning excluded: clean — this fix). The committed unit test is the regression guard.

The multi-turn gateway tool loop's Continue branch fed each round's full
output — including the reasoning item — back into the next inference round
via append_output_items_to_input. Reasoning parsers (e.g. Qwen3) then
re-emit that content wrapped in <think> tags on the visible output_text
channel, leaking model reasoning into the answer the client sees.

Reasoning is ephemeral to the turn that produced it; carry forward only
messages and tool calls. Scoped to the loop carry-forward — the
previous_response_id rehydration path (into_input_items) still preserves
reasoning, as its test asserts.

Fixes vllm-project#120

Signed-off-by: Ashwin Giridharan <girida@amazon.com>
@ashwing ashwing changed the title fix: stop feeding prior-turn reasoning back into the gateway tool loop fix: stop feeding prior-turn reasoning back into the Responses gateway tool loop Jul 15, 2026
@ashwing ashwing marked this pull request as ready for review July 15, 2026 05:14
@jiahuei

jiahuei commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Although current models may only require reasoning for the current turn, it is likely that future models will require prior reasoning turns as well, since that is the progression of frontier closed-sourced models.

  • Claude Opus 4.5 and later Opus models keep all prior thinking blocks. Source
  • OpenAI now has reasoning.context value of all_turns that renders available, compatible reasoning items from earlier turns into the next sample. Source

@ashwing

ashwing commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@jiahuei good point — blanket-dropping breaks preserve-reasoning models (Opus 4.5, OpenAI all_turns). The leak is actually the plaintext reasoning content being fed back, not the item itself — to_input_item clones the whole ReasoningOutput incl. content into the next inference input, and that's what Qwen3 re-emits as <think>. Validated on vLLM (streaming, Qwen3):

round-2 input <think> leak
reasoning with full content 3/3
reasoning, content stripped (id kept) 0/3
reasoning excluded 0/3

So the fix is: keep the item, drop the plaintext content, keep encrypted_content+id — leak-proof and still round-trips signed reasoning. Same to_input_item also runs on the previous_response_id rehydration path, so the leak exists there too; fixing it at that seam covers both.

cc @maralbahari — this moves into the continuation layer you worked on. Thoughts?

@jiahuei

jiahuei commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

I propose to convert this into a feature PR to support reasoning.context parameter, along with the necessary tests.

context: optional, values: "auto" or "current_turn" or "all_turns"
Controls which reasoning items are rendered back to the model on later turns.

When it is set to current_turn, then we can drop prior reasoning turns when sending back the items to upstream.

@maralbahari

Copy link
Copy Markdown
Collaborator

I propose to convert this into a feature PR to support reasoning.context parameter, along with the necessary tests.

context: optional, values: "auto" or "current_turn" or "all_turns" Controls which reasoning items are rendered back to the model on later turns.

When it is set to current_turn, then we can drop prior reasoning turns when sending back the items to upstream.

I agree with this proposal. @ashwing I think it would be best to stick to the api contract.

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.

[SSE Completion - Issue 2] Prior-turn reasoning leaks <think> into the visible answer

3 participants