Skip to content

Python: Preserve OpenAI Responses SSE error metadata - #9616

Open
ZedingZhang (ZedingZhang) wants to merge 3 commits into
microsoft:mainfrom
ZedingZhang:fix/openai-responses-stream-error-metadata
Open

ZedingZhang (ZedingZhang) wants to merge 3 commits into
microsoft:mainfrom
ZedingZhang:fix/openai-responses-stream-error-metadata

Conversation

@ZedingZhang

@ZedingZhang ZedingZhang (ZedingZhang) commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Motivation & Context

Flat Responses API SSE error events currently become text-only RuntimeError instances before being wrapped in ChatClientException. Applications lose the provider's structured error code, message, and parameter when they unwrap the exception, preventing reliable error classification for bounded retries and telemetry.

Description & Review Guide

  • What are the major changes? Retain the original ResponseErrorEvent and its code, message, and param on a private RuntimeError subclass used by the existing error wrapper. Add regression coverage for normal streaming, structured-output streaming, and resumed streaming, including server_error, rate_limit_exceeded, absent codes, and populated or absent parameters.
  • What is the impact of these changes? Callers can recover the structured provider details through ChatClientException.__cause__ without parsing exception text. The existing exception message and RuntimeError compatibility are preserved, as is continuation-token cleanup on a failed resumed stream. These errors remain compatible with shallow/deep copying and pickle round trips, including when nested in ChatClientException; provider details, exception notes and extra instance attributes are retained. There are no new public exports or dependencies.
  • What do you want reviewers to focus on? Confirm that the private error carrier exposes the appropriate provider metadata while preserving the existing exception wrapping and message. The regressions exercise each request path, assert that error events are not emitted as normal updates, and cover shallow/deep copies and pickle for both inner and wrapped exceptions. Exception-note assertions run on Python 3.11 and later.

Related Issue

Fixes #9593

The issue is currently marked reproduced, without a ready-for-implementation label or an explicit maintainer approval comment. This PR presents the focused reproduction and fix for maintainer confirmation. No competing open PR referencing this issue was found at submission.

AI Assistance

  • No material AI assistance was used.
  • This is an AI-assisted contribution. I reviewed, understood, and verified all submitted content and accept responsibility for it.

AI assistance details: Codex assisted with issue analysis, implementation, regression tests, and this PR description. The metadata-loss and exception-serialization regressions were reproduced locally before applying their fixes.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • All unit tests pass, and I have added new tests where possible
  • The PR follows the Contribution Guidelines
  • This PR links to an agreed issue with no competing open PR, or the Related Issue section documents a trivial-change or repository-automation exception.
  • This is not a breaking change. If it is a breaking change, add the breaking change label (or add "[BREAKING]" to the title prefix, before or after any language prefix) — a workflow keeps the label and title prefix in sync automatically.

Copilot AI balanced review requested due to automatic review settings October 23, 2026 03:52
@agent-framework-automation agent-framework-automation Bot added the python Usage: [Issues, PRs], Target: Python label Oct 23, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused change preserves existing exception behavior and covers the affected streaming paths, with no blocking issues identified.

No open issues 🎉

What changed in this PR

Preserves structured metadata from OpenAI Responses SSE error events, addressing #9593 so callers can classify provider errors without parsing exception text.

Changes:

  • Adds a private RuntimeError subclass retaining the original event and its code, message, and param, while preserving existing error text and wrapping.
  • Adds regression coverage for normal, structured-output, and resumed streaming, including continuation-token cleanup and rejection of error events as response updates.
File Description
python/​packages/​openai/​tests/​openai/​test_openai_chat_client.py Tests metadata preservation across streaming paths and error variants.
python/​packages/​openai/​agent_framework_openai/​_chat_client.py Retains provider error metadata in the existing exception chain.

Review effort: Balanced

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ran this offline on head ce92ad4 (same _chat_client.py as 6d2730d).

What I ran:

  • test_openai_chat_client.py on the PR head: 496 passed, 28 skipped.
  • test_streaming_error_event_preserves_provider_details with _chat_client.py reverted to the merge base: all 18 cases fail. So the test does catch the missing metadata.

One defect:

The new exception can't be copied or pickled, and that breaks the ChatClientException that wraps it (python/packages/openai/agent_framework_openai/_chat_client.py:167-180)

_OpenAIResponseStreamError.__init__ takes a ResponseErrorEvent, but it calls super().__init__ with the formatted string. So self.args is (str,). copy.copy and pickle.loads both rebuild the exception as cls(*self.args). That hands a str to __init__, which then reads event.code and fails:

ev = ResponseErrorEvent(type="error", code="server_error", message="boom", param=None, sequence_number=1)
e = _OpenAIResponseStreamError(ev)
copy.copy(e)                     # AttributeError: 'str' object has no attribute 'code'
pickle.loads(pickle.dumps(e))    # AttributeError: 'str' object has no attribute 'code'
w = ChatClientException("wrapped", inner_exception=e)
pickle.loads(pickle.dumps(w))    # AttributeError: 'str' object has no attribute 'code'

The same wrapper round trip works before this PR, when the inner exception is a plain RuntimeError. The issue (#9593) was about callers that classify these errors for retries. Those callers are the ones likely to send the exception across a process boundary or copy it (multiprocessing workers, task queues, result caches), and that now raises inside the error path.

Possible fix: add def __reduce__(self): return (type(self), (self.event,)). ResponseErrorEvent is a pydantic model and pickles fine. A one-line pickle round trip in the new test would cover it.

Not checked: live Azure/OpenAI streams, and whether the maintainers want this type public (it is underscore-private, so callers have to duck-type on inner_exception.code).

@ZedingZhang

ZedingZhang (ZedingZhang) commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

Thanks, Somay kousis (@Somay-kousis)! Follow-up to this review: fixed in 48dc7e115195d7441bfb6efd80283583a94cd253.

The error now reconstructs from the provider event and restores its instance dictionary. Copying and pickle round trips preserve the exception text, provider metadata, notes and extra attributes, including when nested in ChatClientException. Regression coverage exercises shallow/deep copies and pickle for both forms, with note assertions on Python 3.11+.

This branch was successfully deployed

1 active deployment
github-app-auth — 48dc7e11 Deployed Oct 24, 2026 by ZedingZhang via add_label #26110
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

python Usage: [Issues, PRs], Target: Python

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: OpenAI Responses SSE error codes are lost when wrapped in ChatClientException

3 participants