Repository navigation
Python: Preserve OpenAI Responses SSE error metadata - #9616
ZedingZhang (ZedingZhang) wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
🟢 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
RuntimeErrorsubclass retaining the original event and itscode,message, andparam, 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
Somay kousis (Somay-kousis)
left a comment
There was a problem hiding this comment.
Ran this offline on head ce92ad4 (same _chat_client.py as 6d2730d).
What I ran:
test_openai_chat_client.pyon the PR head: 496 passed, 28 skipped.test_streaming_error_event_preserves_provider_detailswith_chat_client.pyreverted 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).
|
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 |
Motivation & Context
Flat Responses API SSE
errorevents currently become text-onlyRuntimeErrorinstances before being wrapped inChatClientException. 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
ResponseErrorEventand itscode,message, andparamon a privateRuntimeErrorsubclass used by the existing error wrapper. Add regression coverage for normal streaming, structured-output streaming, and resumed streaming, includingserver_error,rate_limit_exceeded, absent codes, and populated or absent parameters.ChatClientException.__cause__without parsing exception text. The existing exception message andRuntimeErrorcompatibility 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 inChatClientException; provider details, exception notes and extra instance attributes are retained. There are no new public exports or dependencies.Related Issue
Fixes #9593
The issue is currently marked
reproduced, without aready-for-implementationlabel 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
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
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.