Python: fix(openai): report the background cause when a tool result is rejected - #7603
Conversation
…s rejected A function_call_output chained with previous_response_id is rejected by the Responses API when the predecessor response was created with background=True, even though the predecessor contains the matching function_call. The failure surfaced only as the generic prompt-failure message, which points at the tool result rather than at the background predecessor that actually caused it. Report the cause and the supported alternative when all three signals are present: the pairing error, a previous_response_id continuation, and a background request. Foreground chaining is unaffected and keeps the generic message.
There was a problem hiding this comment.
Pull request overview
Adds targeted diagnostics for background Responses API tool-output pairing failures.
Changes:
- Detects background continuation pairing errors.
- Raises actionable
ChatClientExceptionmessages. - Adds streaming, non-streaming, and negative-control tests.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
python/packages/openai/agent_framework_openai/_chat_client.py |
Adds detection and specialized error handling. |
python/packages/openai/tests/openai/test_openai_chat_client.py |
Adds four diagnostic regression tests. |
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| return error | ||
|
|
||
|
|
||
| async def test_background_tool_output_pairing_error_reports_background_limitation() -> None: |
There was a problem hiding this comment.
Added. docs/specs/004-python-function-calling-loop.md now carries a Rejected tool-output continuation row in the Errors, control flow, and limits matrix, stating the invariant — the specialized guidance carries the background limitation and the orphaned-call_id alternative as conditions to check without asserting how the predecessor was created, and every other pairing rejection keeps the generic transport message — and referencing all five regression tests, including the one added for the review point on the source file.
| return False | ||
| if _TOOL_OUTPUT_PAIRING_ERROR_FRAGMENT not in str(ex).lower(): | ||
| return False | ||
| return bool(run_options.get("background")) and bool(run_options.get("previous_response_id")) |
There was a problem hiding this comment.
Correct, and it changed the patch — though the fix belongs in the message rather than the gate.
You are right that background describes this request, not the response named by previous_response_id. A caller who submits a genuinely orphaned call_id on a background continuation produces all three signals and reaches this branch identically, including the foreground-predecessor case you name.
Gating on tracked predecessor provenance is not available to this client, though. It is stateless with respect to previous_response_id: it holds no map from a response id to how that response was created, and a continuation may be built by a different process, or a different client instance, than the one that created the predecessor. State carried forward from background polling would cover only the subset of continuations this instance itself created, so the branch would still be reachable without it — and would then be inconsistent about when it fires.
So the message no longer asserts anything about the predecessor. It states the background limitation as a condition the caller can check, and names the orphaned-call_id alternative explicitly:
If the preceding response was created with
background=True, this is a service-side limitation: [...] If the preceding response was not a background response, thecall_idabove does not match afunction_callon it.
Both readings are correct whichever case the caller is actually in, and the original error text is still included either way. test_background_tool_output_pairing_error_does_not_assert_predecessor_was_background pins the wording, and the helper's docstring records why the distinction is not decidable here.
|
Thanks for #7603. The clearer diagnostic is useful, but please do not close #7538 as fixed. The PR explicitly adds no fallback or continuation change, so the background tool loop remains broken and currently blocks our workflow. Could the PR drop |
… claim The run options' background flag describes the failing request, not the response named by previous_response_id. A genuinely orphaned call_id on a background continuation reaches the same branch, so the guidance no longer asserts the predecessor was a background response; it gives the background limitation as a condition to check and names the orphaned-call_id alternative. Adds a regression test for that wording and the required scenario-to-test row in the function-calling loop specification.
|
Done — the closing keyword is dropped. The body now reads Agreed on the framing, and thanks for pushing back on it. This PR is diagnostic-only by design: it does not fix the chaining failure, and The two things you want kept alive both belong on #7538 rather than here: the upstream service defect (Azure/azure-sdk-for-python#46092) and the question of whether Agent Framework should offer a supported continuation strategy. Evan Mattson (@moonbox3)'s caution about automatic full-history replay is on that issue too — stored conversation state, reasoning and tool-call history, and local tools potentially running more than once — so a supported fix needs a design decision that is well outside a message change. Your direct-endpoint reproduction table is what makes the upstream attribution stand up, so it is cited in the PR body. |
Motivation & Context
When a local tool loop starts with
background=True, the Responses API rejects the first tool-result continuation with:As established in #7538, the predecessor reaches
status="completed"and retrieving it returns the matchingfunction_call— the emitted, submitted, retrieved and error-reportedcall_idare all identical. The sameprevious_response_idchaining succeeds when the predecessor is created withbackground=False. Gieril Lindi (@Laende) confirmed the failure reproduces directly against Azure OpenAI without APIM and against the rawAsyncOpenAIclient, so the defect is below Agent Framework and is tracked in Azure/azure-sdk-for-python#46092.What Agent Framework controls is the diagnostic. Today the failure arrives as the generic
service failed to complete the promptmessage, which points a reader at the tool result they just submitted rather than at the background predecessor that actually caused the rejection. Thecall_idin the message is present and correct on both sides, so the text actively misleads.This implements the third option in the issue, matching Evan Mattson (@moonbox3)'s guidance to consider "a clearer, targeted diagnostic" rather than an automatic full-history replay. No fallback, retry, or continuation-strategy change is included, so stored conversation state, reasoning and tool-call history, and local tool execution counts are all untouched.
Description & Review Guide
What are the major changes?
_handle_request_errortakes therun_optionsthat produced the failure and, for this one case, raisesChatClientExceptionwith the cause and the supported alternative instead of the generic message. Both existing behaviours are preserved: the content-filter branch is unchanged, and every other error keeps the generic text._is_background_tool_output_pairing_errorrequires all three signals before the message changes: the pairing error text, aprevious_response_idcontinuation, and a background request. The error body carries nocode('code': Nonein the issue's payload), so the message fragment is the only available signal; requiring the other two keeps it from firing on a genuinely orphaned foreground tool result, which is a real user error that should keep the generic message.backgrounddescribes the failing request, not the response named byprevious_response_id, and a stateless client cannot know how that predecessor was created — it may have been produced by a different process. A genuinely orphanedcall_idon a background continuation therefore reaches this branch too, so the message asserts nothing about the predecessor: it gives the background limitation as a condition the caller can check and names the orphaned-call_idalternative.test_background_tool_output_pairing_error_does_not_assert_predecessor_was_backgroundpins that wording.python/AGENTS.md,docs/specs/004-python-function-calling-loop.mdgains aRejected tool-output continuationrow in its errors/continuation matrix covering the invariant and all five regression tests.run_options— they carry no request body to attribute a failure to, so they cannot produce this error.What is the impact of these changes?
run_optionsis an optional parameter on an existing private method, appended last. No new exception type, no new configuration.What do you want reviewers to focus on?
background=Falsefor the tool-calling turn as the supported alternative, sincestore=Falseis not an option for background requests. If you would rather it not recommend a specific workaround, or would rather this be a warning at request time instead of a message on the existing failure, I am happy to change it.code. If you would prefer this keyed on something more stable, I can rework it.Related Issue
Refs #7538
Deliberately not a closing keyword, at Gieril Lindi (@Laende)'s request. This addresses the framework-side diagnostic only, not the chaining defect, so #7538 stays open to track the upstream service fix and the question of a supported continuation strategy.
Verification
Ran against
packages/openai:service failed to complete the promptmessage; the two negative-control tests pass both ways by design, since they assert the generic message is kept.main452 passed / 0 failed / 0 errors, branch 456 passed / 0 failed / 0 errors. The FAILED/ERROR sets are identical (both empty) and the delta is exactly the four new tests.ruff format --diffandruff checkboth clean on the two touched files.Four tests added to
tests/openai/test_openai_chat_client.py, all offline:test_background_tool_output_pairing_error_reports_background_limitationtest_streaming_background_tool_output_pairing_error_reports_background_limitationtest_foreground_tool_output_pairing_error_keeps_generic_messagebackground→ generic messagetest_background_without_previous_response_keeps_generic_messageThe last two are the ones that prove the gate is narrow rather than catching every pairing error.
Contribution Checklist
breaking changelabel (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.