fix: strip orphaned handoff message only for non-official Responses endpoints#3771
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6f9e083146
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| elif item_type in _TOOL_CALL_TO_OUTPUT_TYPE: | ||
| if fresh_reasoning: | ||
| fresh_reasoning = False | ||
| consumed_by_call = True # The reasoning is now consumed by this call. |
There was a problem hiding this comment.
Restrict orphan-message pruning to handoff calls
This treats every tool-call type in _TOOL_CALL_TO_OUTPUT_TYPE as the Azure handoff case, so a non-official Responses client also drops assistant messages emitted alongside ordinary function/custom/shell tool calls. In the normal run_llm_again path, execute_tools_and_side_effects preserves processed_response.new_items and appends the tool output later, producing the same [reasoning, function_call, message, function_call_output] shape for regular tools; this change would silently omit that assistant output from the next request even though the fix is meant for orphaned handoff messages. Please narrow the predicate to handoff calls or otherwise identify the reported orphan shape so regular tool-call assistant output is not lost.
Useful? React with 👍 / 👎.
…ndpoints
When a reasoning-enabled agent hands off, the model emits
[reasoning, function_call, message] in one response. The reasoning is
consumed by the function_call, leaving the trailing assistant message
without its own reasoning item. Strict Responses endpoints (e.g. Azure
OpenAI) reject the replayed shape with HTTP 400 ("Item 'msg_...' of type
'message' was provided without its required 'reasoning' item"), while
official OpenAI tolerates it and, per the Responses API guidance, expects
items since the last user message to be passed through untouched.
Scope the repair to the Responses model layer, applied only when the client
is not official OpenAI (is_official_openai_client). Official OpenAI and the
Chat Completions/LiteLLM paths are untouched. The message also stays in
session history -- it is only omitted from the outgoing payload to a strict
endpoint, where keeping it would otherwise 400 the entire turn.
Add a test that reproduces the actual Azure 400 and proves the fix prevents
it, plus endpoint-scoping and pure-function unit tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
6f9e083 to
be8e305
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: be8e305124
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| elif item_type == "message": | ||
| if not consumed_by_call or item.get("role") != "assistant": | ||
| result.append(item) |
There was a problem hiding this comment.
Clear reasoning state after preserving messages
When this helper runs over a non-official Responses replay, a normal prior output shaped like [reasoning, assistant message] leaves fresh_reasoning set because the message branch preserves the message without consuming the reasoning. If a later turn in the same input then contains a tool call without its own reasoning followed by an assistant message before the call output, this stale reasoning makes the later call look like it consumed reasoning and the legitimate assistant message is stripped from the request. Clear fresh_reasoning when a preserved assistant message consumes the current reasoning item so the state cannot bleed across items/turns.
Useful? React with 👍 / 👎.
|
Can you share the repro steps of the error? |
Summary
Reasoning-enabled handoffs emit
[reasoning, function_call, message]in a single response. Thereasoning is consumed by the
function_call, so the trailing assistant message has no reasoning ofits own. Strict Responses endpoints (e.g. Azure OpenAI) reject the replayed shape with HTTP 400
(
Item 'msg_...' of type 'message' was provided without its required 'reasoning' item), whileofficial OpenAI tolerates it and — per the Responses API guidance — expects items since the last
user message to be passed through untouched.
This scopes the repair to the Responses model layer, applied only when the client is not
official OpenAI (
is_official_openai_client). Official OpenAI and the Chat Completions / LiteLLMpaths are untouched (the latter already fold reasoning into the assistant message and never emit
standalone reasoning items). The message also stays in session history — it is only omitted from the
outgoing payload to a strict endpoint, where keeping it would otherwise 400 the entire turn.
This supersedes #3574, which applied the drop globally across every provider/path; that was closed
because it could discard legitimate assistant output beyond the reported Azure case.
Test plan
test_non_official_endpoint_avoids_reasoning_item_400_on_handoffreproduces the actual Azure 400with a strict fake client and proves the fix prevents it end-to-end.
test_non_official_endpoint_strips_orphaned_handoff_message/test_official_endpoint_preserves_orphaned_handoff_messagepin the endpoint scoping.drop_orphaned_messages_after_consumed_reasoning(assistant-onlydrop, user-message preservation, multiple orphans, no-consume, cross-turn reset).
make lint,make format,make typecheck,make testspass.Issue number
Supersedes #3574.
Checks
make lintandmake format