Skip to content

fix: preserve finalized output on null response completion - #3345

Merged
marcuswood-oai merged 5 commits into
openai:mainfrom
LeSingh1:fix-parse-response-null-output
Sep 10, 2026
Merged

fix: preserve finalized output on null response completion#3345
marcuswood-oai merged 5 commits into
openai:mainfrom
LeSingh1:fix-parse-response-null-output

Conversation

@LeSingh1

@LeSingh1 LeSingh1 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Responses parsing raised TypeError when a response contained output: null. Treat null output as empty for standalone parsing, and recover finalized response.output_item.done items when a streaming completion has null or missing output.

Recovery preserves item order, final statuses, annotations, refusals, structured output, tool arguments, and completion metadata. Explicit completion output, including [], remains authoritative. This addresses the review feedback about losing streamed output without adding per-item finalization branches.

Fixes #3325.

Validation

  • Merged current main into the contributor branch.
  • Responses suite: 48 passed with Pydantic v2 and 48 passed with Pydantic v1 on Python 3.10.
  • Public synchronous and asynchronous streaming coverage includes null, missing, empty, and supplied completion output, with and without streamed items.
  • Ruff, Pyright (0 errors), Mypy (1,614 source files), and import check passed.
  • Full PR diff reviewed; no unrelated changes or security findings.

parse_response iterates response.output directly. Response.output is
typed as a required list, but the SDK constructs models leniently, so
when a backend emits a terminal response.completed event with output
null (seen with the Codex backend for incomplete responses) the field
becomes None and the loop raises TypeError: 'NoneType' object is not
iterable. This is the streaming responses.stream() / responses.parse()
crash path.

Guard the loop with `response.output or []` so a response with no output
items parses to an empty output list.
@LeSingh1
LeSingh1 requested a review from a team as a code owner June 1, 2026 05:52

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1f53041a27

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

output_list: List[ParsedResponseOutputItem[TextFormatT]] = []

for output in response.output:
for output in response.output or []:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve streamed output when completed event omits output

In the streaming path, ResponseStreamState.accumulate_event builds up a snapshot from response.output_item.added and delta events, but on response.completed it calls parse_response(response=event.response). When the backend sends the terminal event with output: null after earlier streamed output items—the case described by this test—this coercion turns the final parsed response into output == [], so get_final_response() and the emitted response.completed event silently lose the text/tool calls already accumulated in the stream instead of parsing from the snapshot.

Useful? React with 👍 / 👎.

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.

Addressed in 859e182. The stream now retains finalized response.output_item.done payloads by output index. When completion output is null or missing, it supplies those items to a shallow copy of the terminal response and runs the existing parser. This preserves final statuses, annotations, refusals, structured output, tool arguments, and completion metadata without reconstructing final items from partial deltas. Explicit completion output, including [], remains authoritative.

Regression coverage exercises the public sync and async streaming APIs with null, missing, empty, and supplied completion output, both with and without streamed items. The Responses suite passes with Pydantic v1 and v2 (48 tests each); Ruff, Pyright, and Mypy also pass. Recovery requires finalized item events; it does not promote unfinished deltas into completed output.

When a backend sends the terminal response.completed event with a null or
empty output after streaming output items, the streaming path passed that
response straight to parse_response, so the final response dropped the
text and tool calls already accumulated in the snapshot. Fall back to the
accumulated snapshot output in that case.

Addresses the streaming review note on this PR.
@LeSingh1

LeSingh1 commented Jun 1, 2026

Copy link
Copy Markdown
Contributor Author

Good catch on the streaming path. The first commit only guarded parse_response against a null output, which would have collapsed the final response to an empty list when items were streamed before a null-output completed event.

I pushed a follow-up: in ResponseStreamState.accumulate_event, when the terminal response.completed event has no output but the snapshot already accumulated output items, it now falls back to the snapshot output before parsing, so streamed text and tool calls are preserved. Added a streaming test (test_streaming_completed_with_null_output_keeps_snapshot) that drives the state through created/output_item.added/content_part.added/output_text.delta and then a completed event with output null, asserting the accumulated text survives. Full tests/lib/responses suite passes.

@marcuswood-oai marcuswood-oai changed the title Handle null output in parse_response fix: preserve finalized output on null response completion Sep 9, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 9, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-09T23:59:20.814272Z 364745a New commits
🔒 Security Review Completed 2026-09-09T23:59:34.336107Z 364745a New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@marcuswood-oai marcuswood-oai 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.

Reviewed the full diff at 859e182. The fix preserves finalized streamed output when the completion payload has null or missing output, while keeping explicit empty and populated output authoritative. The earlier automated review feedback is addressed, and the diff review found no security concerns.

Validation:

  • 48 Responses tests passed under each of Pydantic v1 and v2, including sync/async null, missing, empty, and populated terminal output cases.
  • Ruff, Pyright, Mypy, and import checks passed locally.
  • All 40 live smoke checks passed: 20 against this PR and 20 against current main, covering sync/async requests, structured parsing, streaming, structured streaming, and function calls across Pydantic v1/v2.

No public signatures, types, or dependencies change, and no regression was observed in the tested paths. CI is still running; this approval does not authorize merging. Please leave the PR unmerged pending explicit maintainer instruction.

pull Bot pushed a commit to Superoldman96/openai-python that referenced this pull request Sep 9, 2026
## Summary

Fork PRs openai#3760 and openai#3345 have green Python CI but no required Castiron
budget statuses: both the workflow-run association and commit-to-PR
endpoint return no PRs. The trusted evaluator fails with `source run
must identify exactly one current PR targeting main`, and the publisher
returns without posting statuses.

Fall back to listing open PRs by the source run's fork owner and branch,
then retain the existing exact-head, target, freshness, and ambiguity
checks. Apply the fallback to report computation, budget evaluation, and
publication; update the reporter integrity digest.

## Validation

- Regression tests reproduce the evaluator failure and missing statuses
on unpatched main.
- Patched read-only lookup resolves the exact current heads of both
affected PRs.
- Castiron reporter/budget suite: 53 tests, OK (1 optional
compiler-contract test skipped), including the JavaScript publishers,
pagination, stale heads, wrong targets, and ambiguous matches.
- Ruff, Pyright, and mypy for `scripts/castiron`.

The trusted handler runs from main. After this fix merges, rerun the
Castiron custom-code workflows for openai#3760 and openai#3345 to publish their
required statuses.
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Castiron custom code

✅ No new custom-code files detected.

36 mixed files remain; 0 existing customizations changed.

Compared 397ea08d8cf1364745a3a0e3. Generated baselines verified.

36 existing customizations unchanged
  • api.md
  • scripts/castiron/README.md
  • scripts/castiron/custom_code_report.py
  • scripts/castiron/test_custom_code_report.py
  • src/openai/init.py
  • src/openai/_client.py
  • src/openai/resources/audio/transcriptions.py
  • src/openai/resources/audio/translations.py
  • src/openai/resources/beta/beta.py
  • src/openai/resources/beta/responses/responses.py
  • src/openai/resources/beta/threads/runs/runs.py
  • src/openai/resources/beta/threads/threads.py
  • src/openai/resources/chat/completions/completions.py
  • src/openai/resources/embeddings.py
  • src/openai/resources/files.py
  • src/openai/resources/realtime/realtime.py
  • src/openai/resources/responses/responses.py
  • src/openai/resources/uploads/uploads.py
  • src/openai/resources/vector_stores/file_batches.py
  • src/openai/resources/vector_stores/files.py
  • src/openai/resources/videos.py
  • src/openai/resources/webhooks/init.py
  • src/openai/resources/webhooks/webhooks.py
  • src/openai/types/chat/init.py
  • src/openai/types/chat/chat_completion_message_tool_call.py
  • src/openai/types/fine_tuning/fine_tuning_job_integration.py
  • src/openai/types/responses/init.py
  • src/openai/types/responses/response.py
  • src/openai/types/responses/response_function_web_search.py
  • src/openai/types/responses/response_function_web_search_param.py
  • src/openai/types/responses/responses_client_event.py
  • src/openai/types/responses/responses_client_event_param.py
  • src/openai/types/responses/tool.py
  • src/openai/types/responses/tool_param.py
  • src/openai/types/webhooks/init.py
  • tests/api_resources/test_videos.py

A changed generated baseline means this report cannot reliably identify which handwritten lines changed.

Inspect the custom-code diff

Download the exact patch produced by this run (requires repository access):

gh run download 34419242671 --repo openai/openai-python \
  --name castiron-custom-code-34419242671-1 --dir /tmp/castiron-custom-code-34419242671-1
git apply --stat /tmp/castiron-custom-code-34419242671-1/custom-code.patch
cat /tmp/castiron-custom-code-34419242671-1/custom-code.patch

Or reproduce it from an SDK checkout containing the vendored reporter:

git fetch --no-tags origin 397ea08d8cf151039c069c1173f5de57cff5c081 364745a3a0e3edf9392f46085018776a9400d893
python3 scripts/castiron/custom_code_report.py report \
  --base 397ea08d8cf151039c069c1173f5de57cff5c081 \
  --head 364745a3a0e3edf9392f46085018776a9400d893 --fetch --require-head-hash --public \
  --out /tmp/castiron-custom-code-364745a3a0e3
cat /tmp/castiron-custom-code-364745a3a0e3/custom-code.patch

This is the current full custom patch for mixed files, not an attribution of only the handwritten lines changed by this PR.

Full report and patch

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 364745a3a0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +362 to +363
elif event.type == "response.output_item.done":
self._completed_output[event.output_index] = event.item

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Release finalized-item cache after completion

For every normal stream containing response.output_item.done events, this dictionary retains the complete finalized item models even after response.completed has been parsed, including when the terminal response already supplies authoritative output. Keeping the stream object alive therefore retains an extra full copy of potentially large message, tool, or image data; clear this recovery-only cache once completion parsing has consumed it.

AGENTS.md reference: AGENTS.md:L114-L119

Useful? React with 👍 / 👎.

adhavan18 added a commit to adhavan18/openai-python that referenced this pull request Sep 10, 2026
openai#3345 fixed the null-output crash in parse_response() and the streaming
accumulator, but response.output_text still iterates self.output directly
and still appends content.text without checking it's not None, so
output_text('output': null) still raises TypeError, and a partial/aborted
output_text content block (text: null) crashes the str.join instead of
just being skipped.

Same fix shape as openai#3345: guard with `self.output or []`, and skip a
content.text that's None rather than appending it.
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.

parse_response crashes with TypeError when response.output is null in response.completed event (chatgpt.com Codex backend)

2 participants