Repository navigation
Conversation
…ol calls OpenAIResponsesLlm never sent a reasoning model's reasoning back to OpenAI: `_content_to_response_input_items` skipped every thought part on replay (the reasoning item ids were only recorded at response level, and synthetic ids are rejected), and `previous_response_id` is never set for this model -- the flow only fills `previous_interaction_id` for the Gemini interactions API. In every tool loop the model restarted its reasoning from scratch after each tool call, and with `store: false` the encrypted reasoning state was lost entirely. Capture the reasoning item id and encrypted_content in a JSON envelope on the first thought part of each captured item (thought_signature, the same slot that already carried the encrypted payload; one envelope per item, so a rebuilt input never repeats a reasoning id). Rebuild the `reasoning` input item at the part's original position, in front of the function call it came with. Parts without a captured envelope -- synthetic thoughts, pre-envelope sessions, other providers' signatures -- keep the existing skip path unchanged. Fixes google#8110
A captured reasoning item was replayed whenever it had an item id, which can send items the Responses API rejects: after the agent switched models, after a history edit removed the follow-up the item preceded, or with store disabled and no encrypted payload. Guard replay on three conditions: - Same model: the replay envelope now also records the response-level model, and a captured item is replayed only for the same model family (a trailing -YYYY-MM-DD snapshot suffix is ignored, so gpt-5 and gpt-5-2025-08-07 are one family). Envelopes without a recorded model -- captured before this change, or by a backend that does not report one -- still replay, so existing sessions are unaffected. - Not orphaned: a post-pass over the assembled request input drops a replayed reasoning item whose first following non-reasoning item is not the function call or assistant message it precedes (for example after a history edit removed the follow-up, or when a user turn comes next). Consecutive reasoning items are one block anchored by whatever follows them. - Resolvable with store=false: the server keeps no reasoning state, so an id-only item is dropped with a warning pointing at include=['reasoning.encrypted_content']; items that carry encrypted_content, and the default store setting, are unaffected. The envelope's new model field is additive: readers of the previous revision ignore it, and envelopes without it replay as before.
|
Thanks @surajksharma07 -- all three points are addressed in abf1e24:
Verified RED→GREEN: with the production changes stashed the 8 new tests fail as expected; restored, the test file is 96/96 green and the openai integration suite is 307/307. pyink and ruff clean. Replaying the raw item wholesale (preserving phase, etc.) is left as a follow-up per your suggestion. |
Please ensure you have read the contribution guide before creating a pull request.
Link to Issue or Description of Change
Problem
Reasoning models on the Responses API return their reasoning as separate output items, and OpenAI's function-calling guide requires clients to pass those items back with the tool call outputs.
OpenAIResponsesLlmnever did, so in every tool loop the model restarted its reasoning from scratch after each tool call, and withstore: falsethe encrypted reasoning state was lost entirely. Two gaps combined:_reasoning_partsstored the item'sencrypted_contenton each thought part'sthought_signature, but the reasoning itemidwas recorded only in the response-levelcustom_metadata, which the request builder never reads._content_to_response_input_itemsskipped every thought part on replay (its docstring: without real item IDs, synthetic ones are rejected), andprevious_response_idis never set for this model — the flow only fillsprevious_interaction_idfor the Gemini interactions API — so continuity was neither replayed nor chained.Solution
_reasoning_partsnow stores a JSON envelope{"adk_openai_reasoning": {"id", "encrypted_content"}}on the first thought part of each captured reasoning item (at most one envelope per item, so a rebuilt input never contains duplicate reasoning ids). The envelope is built only when the item has anid: the Responses API requires reasoning input items to reference a real item, so id-less captures keep today's behavior instead of risking a 400. The same slot already carried the encrypted payload, so session persistence and the streaming paths (response.completedand theoutput_item.doneaccumulator branch) are covered without further changes._content_to_response_input_itemsrebuilds a{type: "reasoning", id, summary, encrypted_content}input item at the part's original position, in front of the function call it came with, whenever a part carries the envelope. Everything without an envelope — synthetic thoughts, plain summaries, pre-envelope sessions that stored raw encrypted bytes, other providers' signatures — takes the pre-existing skip path unchanged, preserving the guard's original rationale.previous_response_idhandling is untouched; when ADK later wiresprevious_interaction_idfor this model, it composes independently.Known limitations:
thought_signature; those parts stay skipped (no real id to reference).LlmRequest.previous_interaction_id(no ADK flow does for this model today), replayed reasoning items are still included ininputalongsideprevious_response_id.Testing Plan
Unit Tests:
8 new tests in
tests/unittests/integrations/openai/test_openai_responses_llm.py: envelope capture (first-part-only across multiple summary segments, id-less items capture nothing), outbound replay (captured item rebuilt in front of its function call, encrypted-only carrier, id-only stored-item reference, envelope without id stays skipped), a two-turn test throughgenerate_content_asyncwith a mocked client asserting the second request'sinputcarries the reasoning item in position, and a streaming test covering theoutput_item.doneaccumulator path. The two pre-existing tests pinning the skip behavior for synthetic parts pass unchanged; the three inbound assertions were updated from raw signature bytes to the envelope payload.pytest summaries:
tests/unittests/integrations/openai/: 299 passed.tox(py311/py312/py313/py314, full suite): 24517 / 24509 / 24509 / 24511 passed. 13 tests fail in every environment — the same network-path tools tests (SSRF/DNS pinning intest_rest_api_tool.pyandtest_load_web_page.py, plus one MCP google-auth transport test) that fail identically on pristinemainin the same virtualenvs (A/B-verified when preparing fix(models): replay OpenRouter reasoning_details in LiteLlm histories #8111); zero failures inintegrations/.Manual End-to-End (E2E) Tests:
inputcontains noreasoningitem); restored, it passes.contributing/samples/models/openai_responses_reasoningagent with a multi-step tool prompt and capture the second request (subclassOpenAIResponsesLlmand log[i.get("type") for i in kwargs["input"]]from_get_response_create_kwargs); with this change each request after a tool call includes the earlierreasoningitems in their original positions.Checklist
Additional context
Sibling fix #8111 addresses the same class of loss in the
LiteLlmadapter (OpenRouterreasoning_details); this PR only changes the OpenAI Responses integration.