Skip to content

fix: strip orphaned handoff message only for non-official Responses endpoints#3771

Draft
utkarshkr100 wants to merge 1 commit into
openai:mainfrom
utkarshkr100:fix/orphaned-handoff-message-responses-scoped
Draft

fix: strip orphaned handoff message only for non-official Responses endpoints#3771
utkarshkr100 wants to merge 1 commit into
openai:mainfrom
utkarshkr100:fix/orphaned-handoff-message-responses-scoped

Conversation

@utkarshkr100

Copy link
Copy Markdown

Summary

Reasoning-enabled handoffs emit [reasoning, function_call, message] in a single response. The
reasoning is consumed by the function_call, so the trailing assistant message has no reasoning of
its 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), while
official 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 / LiteLLM
paths 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_handoff reproduces the actual Azure 400
    with 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_message pin the endpoint scoping.
  • Five pure-function unit tests for drop_orphaned_messages_after_consumed_reasoning (assistant-only
    drop, user-message preservation, multiple orphans, no-consume, cross-turn reset).
  • make lint, make format, make typecheck, make tests pass.

Issue number

Supersedes #3574.

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation (no user-facing docs change)
  • I've run make lint and make format
  • I've made sure tests pass

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +220 to +223
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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>
@utkarshkr100 utkarshkr100 force-pushed the fix/orphaned-handoff-message-responses-scoped branch from 6f9e083 to be8e305 Compare July 9, 2026 16:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +230 to +232
elif item_type == "message":
if not consumed_by_call or item.get("role") != "assistant":
result.append(item)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@seratch

seratch commented Jul 9, 2026

Copy link
Copy Markdown
Member

Can you share the repro steps of the error?

@seratch seratch marked this pull request as draft July 9, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants