fix(streaming): return final response for incomplete and failed events#3538
fix(streaming): return final response for incomplete and failed events#3538hsusul wants to merge 2 commits into
Conversation
get_final_response() previously required response.completed only, so max_output_tokens truncation and failed streams raised RuntimeError even though those terminal events already carry a full Response payload.
gnanirahulnutakki
left a comment
There was a problem hiding this comment.
Reviewed exact commit 9ebe10435f1184ca0852cadac531c3121fb491a5. The existing targeted suite passes (19 tests) and the changed files pass Ruff, but an isolated structured-output reproduction exposes one correctness issue on the new incomplete/failed paths.
| input_tools=self._input_tools, | ||
| ) | ||
| elif event.type == "response.incomplete": | ||
| self._completed_response = parse_response( |
There was a problem hiding this comment.
Avoid parsing truncated structured output on terminal failures
parse_response() calls the strict parse_text() path for every output text, so this raises before the terminal response can be stored whenever an incomplete structured output contains partial JSON. On this exact head I reproduced it with text_format=Answer and a response.incomplete payload whose text is {"value": and incomplete_details.reason == "max_output_tokens"; handle_event() raises Pydantic ValidationError. The same happens for response.failed, while the base commit accepts both events without attempting the parse. Consequently get_final_response() still cannot return the terminal response in the structured-output case this change is meant to fix. All new tests use text_format=omit, so they do not cover this branch. Please preserve the terminal response without strict structured parsing (for example, leave parsed unset for non-completed responses) and add incomplete/failed tests with a supplied Pydantic text format.
There was a problem hiding this comment.
Thanks — confirmed and fixed on this PR.
For response.incomplete / response.failed we now store the terminal response via parse_response(..., text_format=omit, input_tools=omit) so truncated structured JSON (and truncated tool args) no longer raise before get_final_response() can return. response.completed still uses the strict text_format path.
Added regression coverage with text_format=_Answer and truncated payload text {"value": for both incomplete and failed, plus sync/async get_final_response() cases and a completed-path sanity check that still parses successfully.
When text_format is set, parse_response() was still applied to response.incomplete / response.failed payloads, so truncated structured JSON raised ValidationError before get_final_response() could return. Store those terminal responses without strict text/tool parsing.
gnanirahulnutakki
left a comment
There was a problem hiding this comment.
Verified the fix on e896c03a2ac9136bfc348195faadf4b037194b28. Incomplete and failed terminal responses now bypass strict structured-output and tool-argument parsing, while the completed path still parses normally. The focused suite passes all 19 tests, including the new sync/async truncated-Pydantic cases, and both changed files pass Ruff lint and format checks.
Changes being requested
Summary
ResponseStream.get_final_response()/AsyncResponseStream.get_final_response()only stored a finalParsedResponsewhen aresponse.completedevent arrived. Terminalresponse.incompleteandresponse.failedevents already carry a fullResponsepayload (for example aftermax_output_tokenstruncation), but callingget_final_response()after those streams still raised:This change treats
response.incompleteandresponse.failedas terminal events inResponseStreamState.accumulate_event, matching:ResponseAccumulator(response.completed|response.failed|response.incomplete)get_final_completion(), which returns the snapshot regardless offinish_reasonThe touched code lives under
src/openai/lib/, which CONTRIBUTING.md states is not modified by the generator.Problem
Minimal reproduction (no API key):
Current behavior
response.completed→_completed_responseis set;get_final_response()succeedsresponse.incomplete/response.failed→_completed_responsestaysNone;get_final_response()raisesRuntimeErrorCorrected behavior
All three terminal events populate
_completed_responseviaparse_response(...). Sync and asyncget_final_response()return that parsed response. If no terminal event was seen, the error message now says a terminal response event was missing.Root cause
accumulate_eventonly handledresponse.completedwhen assigning_completed_response.Implementation
Smallest change in
src/openai/lib/streaming/responses/_responses.py:response.incompleteandresponse.failedget_final_response()RuntimeErrormessageNo public request/response shapes changed. No generated API resources/types edited.
Regression tests
Added
tests/lib/responses/test_response_stream_final.py:get_final_response()for all three terminal eventsValidation
Pre-fix (expected failures on incomplete/failed paths):
Post-fix:
Not run / limitations:
./scripts/testagainst the Steady mock server (requires./scripts/mock)./scripts/lintvia Rye (local validation used the project.venvtools equivalent to ruff/pyright/mypy/import check for the changed files)upstream/mainas well (pre-existing; unrelated)Compatibility
response.completedstreams are unchangedRuntimeErroron incomplete/failed streams now receive the terminalParsedResponse(inspectstatus/incomplete_details/erroras needed)Additional context & links
upstream/main@8a6adcb3(release 2.48.0)_completed_responsefor incomplete/failedopenai-nodeResponseAccumulatorhandlesresponse.completed|response.failed|response.incompletetogether