Skip to content

fix(streaming): return final response for incomplete and failed events#3538

Open
hsusul wants to merge 2 commits into
openai:mainfrom
hsusul:fix/get-final-response-terminal-events
Open

fix(streaming): return final response for incomplete and failed events#3538
hsusul wants to merge 2 commits into
openai:mainfrom
hsusul:fix/get-final-response-terminal-events

Conversation

@hsusul

@hsusul hsusul commented Jul 24, 2026

Copy link
Copy Markdown
  • I understand that this repository is auto-generated and my pull request may not be merged

Changes being requested

Summary

ResponseStream.get_final_response() / AsyncResponseStream.get_final_response() only stored a final ParsedResponse when a response.completed event arrived. Terminal response.incomplete and response.failed events already carry a full Response payload (for example after max_output_tokens truncation), but calling get_final_response() after those streams still raised:

RuntimeError: Didn't receive a `response.completed` event.

This change treats response.incomplete and response.failed as terminal events in ResponseStreamState.accumulate_event, matching:

  • the OpenAI Node SDK ResponseAccumulator (response.completed | response.failed | response.incomplete)
  • Assistants streaming helpers, which update the final run on incomplete/failed
  • Chat Completions get_final_completion(), which returns the snapshot regardless of finish_reason

The touched code lives under src/openai/lib/, which CONTRIBUTING.md states is not modified by the generator.

Problem

Minimal reproduction (no API key):

from openai import omit
from openai._models import construct_type
from openai.types.responses import ResponseCreatedEvent, ResponseIncompleteEvent
from openai.lib.streaming.responses._responses import ResponseStreamState

minimal = {
    "id": "resp_test",
    "object": "response",
    "created_at": 0,
    "status": "in_progress",
    "model": "gpt-4o-mini",
    "output": [],
    "parallel_tool_calls": True,
    "tool_choice": "auto",
    "tools": [],
}

state = ResponseStreamState(input_tools=omit, text_format=omit)
state.handle_event(construct_type(type_=ResponseCreatedEvent, value={
    "type": "response.created",
    "sequence_number": 0,
    "response": minimal,
}))
state.handle_event(construct_type(type_=ResponseIncompleteEvent, value={
    "type": "response.incomplete",
    "sequence_number": 1,
    "response": {**minimal, "status": "incomplete"},
}))
assert state._completed_response is None  # bug: should be the incomplete ParsedResponse

Current behavior

  • response.completed_completed_response is set; get_final_response() succeeds
  • response.incomplete / response.failed_completed_response stays None; get_final_response() raises RuntimeError

Corrected behavior

All three terminal events populate _completed_response via parse_response(...). Sync and async get_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_event only handled response.completed when assigning _completed_response.

Implementation

Smallest change in src/openai/lib/streaming/responses/_responses.py:

  • store the parsed response for response.incomplete and response.failed
  • document terminal-event requirement on sync/async get_final_response()
  • update the missing-terminal-event RuntimeError message

No public request/response shapes changed. No generated API resources/types edited.

Regression tests

Added tests/lib/responses/test_response_stream_final.py:

  • stream state stores final response for completed / incomplete / failed
  • no terminal event → no final response
  • sync + async get_final_response() for all three terminal events
  • sync + async error when the stream ends without a terminal event

Validation

Pre-fix (expected failures on incomplete/failed paths):

PYTHONPATH=src python -m pytest tests/lib/streaming/test_response_stream_final.py -q
# 8 failed, 4 passed

Post-fix:

PYTHONPATH=src python -m pytest tests/lib/responses/test_response_stream_final.py tests/lib/responses/test_responses.py -q
# 19 passed

ruff format --check <changed files>   # pass
ruff check <changed files>            # pass
./scripts/run-pyright src/openai/lib/streaming/responses/_responses.py tests/lib/responses/test_response_stream_final.py
# 0 errors
mypy src/openai/lib/streaming/responses/_responses.py
# Success
python -m build
# Successfully built openai-2.48.0.tar.gz and openai-2.48.0-py3-none-any.whl
git diff --check
# clean

Not run / limitations:

  • Full ./scripts/test against the Steady mock server (requires ./scripts/mock)
  • Full ./scripts/lint via Rye (local validation used the project .venv tools equivalent to ruff/pyright/mypy/import check for the changed files)
  • Chat completions inline-snapshot tests fail under pytest-xdist on clean upstream/main as well (pre-existing; unrelated)

Compatibility

  • Successful response.completed streams are unchanged
  • Callers that previously got RuntimeError on incomplete/failed streams now receive the terminal ParsedResponse (inspect status / incomplete_details / error as needed)
  • Exception message text for the no-terminal-event case changed slightly

Additional context & links

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.
@hsusul
hsusul requested a review from a team as a code owner July 24, 2026 05:38

@gnanirahulnutakki gnanirahulnutakki 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.

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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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 gnanirahulnutakki 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.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants