Skip to content

fix(parsing): guard against None response.output in parse_response - #3517

Open
rkfshakti wants to merge 10 commits into
openai:mainfrom
rkfshakti:fix/azure-aad-bearer-token
Open

fix(parsing): guard against None response.output in parse_response#3517
rkfshakti wants to merge 10 commits into
openai:mainfrom
rkfshakti:fix/azure-aad-bearer-token

Conversation

@rkfshakti

Copy link
Copy Markdown

Problem

The chatgpt.com Codex backend sometimes sends response.output: null in the consolidated response.completed event, even when valid output_item.done events were streamed earlier. The SDK then raises TypeError: 'NoneType' object is not iterable inside the stream accumulator, killing the entire stream before the consumer can read the deltas.

Closes #3325.

Fix

Change for output in response.output to for output in response.output or [] so that None is handled gracefully (empty iteration instead of TypeError).

Testing

  • python -c "import ast; ast.parse(open('src/openai/lib/_parsing/_responses.py').read())" passes
  • parse_response(response=Response(output=None, ...)) no longer raises TypeError

Checklist

  • Single-file change
  • No new dependencies
  • Commit references the issue

The chatgpt.com Codex backend sometimes sends response.output: null in
the consolidated response.completed event, even when valid output_item.done
events were streamed earlier. The SDK then raises TypeError: 'NoneType'
object is not iterable inside the stream accumulator, killing the entire
stream before the consumer can read the deltas.

Fix: iterate over response.output or [] instead of response.output directly.

Closes openai#3325
@rkfshakti
rkfshakti requested a review from a team as a code owner July 19, 2026 10:40

@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: cb666413b4

ℹ️ 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".

Comment thread src/openai/lib/_parsing/_responses.py Outdated
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 output is null

When this is called from ResponseStreamState.accumulate_event for a response.completed event whose event.response.output is None, response.output or [] turns the final ParsedResponse into an empty output list. The stream has already accumulated output items/text in snapshot.output, but get_final_response() and ResponseCompletedEvent use _completed_response from this parse, so these streams finish with response.output/output_text empty instead of the text or tool calls just emitted.

Useful? React with 👍 / 👎.

@rkfshakti

Copy link
Copy Markdown
Author

Thanks for reviewing! 👋

This is my first contribution to the OpenAI Python SDK. I'm actively contributing to GenAI open-source projects — you can see my other PRs at https://github.com/rkfshakti (chroma-core/chroma, langchain-ai/langchain, langgenius/dify, NousResearch/hermes-agent, firecrawl/firecrawl).

The fix is minimal — response.output can be null in the response.completed event from the chatgpt.com Codex backend, and for output in response.output raises TypeError. Changing to response.output or [] handles this gracefully.

Happy to add a test case if that would help. Thanks!

…as null output

The chatgpt.com Codex backend sometimes sends response.output: null in the
consolidated response.completed event even when valid output_item.done events
were streamed earlier (issue openai#3325). The previous fix (response.output or [])
prevented the TypeError but discarded the already-streamed snapshot.output,
causing the final ParsedResponse to have empty output/output_text.

Move the guard into ResponseStreamState.accumulate_event: when
event.response.output is None and the snapshot has accumulated output items,
build the completed response from the snapshot instead of calling
parse_response with an empty output list. This preserves streamed text and
tool calls in get_final_response() and ResponseCompletedEvent.

Addresses Codex review feedback on openai#3517.
@rkfshakti

Copy link
Copy Markdown
Author

Thanks for the Codex review — the P2 point about preserving streamed output is correct and I've pushed a fix (1c050f69).

Problem with the previous approach: response.output or [] in parse_response() prevented the TypeError but discarded the already-accumulated snapshot.output. Since get_final_response() and ResponseCompletedEvent use _completed_response from parse_response(), streams finished with empty output/output_text even when text or tool calls had been emitted earlier via output_item.done events.

New fix: Moved the guard into ResponseStreamState.accumulate_event() (src/openai/lib/streaming/responses/_responses.py). When event.response.output is None and the snapshot has accumulated output items, the completed response is built from snapshot.output instead of calling parse_response() with an empty output list. This preserves the streamed text and tool calls in the final ParsedResponse.

The parse_response() change has been reverted — it now uses response.output directly again, since the null case is handled upstream in the streaming accumulator where the snapshot is available.

Happy to add a streaming test that exercises the response.completed-with-null-output path if the maintainers would like regression coverage.

@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: 1c050f6988

ℹ️ 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".

Comment on lines +367 to +372
self._completed_response = construct_type_unchecked(
type_=ParsedResponse[TextFormatT],
value={
**event.response.to_dict(),
"output": [item.to_dict() for item in snapshot.output],
},

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 Run response parsing on the streamed fallback

When a streaming response.completed has response.output == None and the caller supplied a text_format or parseable/strict function tool, this fallback constructs the final ParsedResponse directly from snapshot.output instead of passing that output through parse_response. The streamed snapshot only accumulates raw text and raw function-call arguments, so get_final_response() and the ResponseCompletedEvent.response will preserve output_text but leave output_parsed/parsed_arguments as None in exactly the null-output case this branch handles.

Useful? React with 👍 / 👎.

# Calling `parse_response` with a null `output` would discard the
# already-accumulated `snapshot.output` and emit an empty final
# response, so we fall back to the streamed snapshot in that case.
if event.response.output is None and snapshot.output:

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 Handle null completed output without streamed items

If the backend sends response.output: null on response.completed before any output item has been accumulated, this condition is false and the else path still calls parse_response with event.response.output is None, reintroducing the same TypeError this fix is meant to avoid. This affects legitimately empty completed responses (or any null-output stream with no prior response.output_item.added) instead of returning a parsed response with an empty output list.

Useful? React with 👍 / 👎.

type_=ParsedResponse[TextFormatT],
value={
**event.response.to_dict(),
"output": [item.to_dict() for item in snapshot.output],

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 Apply done events before serializing the fallback snapshot

When response.completed has a null output after normal streamed *.done events, this fallback serializes snapshot.output, but the accumulator above never applies response.output_item.done, response.content_part.done, or response.function_call_arguments.done to that snapshot. As a result, get_final_response() can expose stale in_progress item statuses or miss final content metadata carried by the done events even though those finalized items were already streamed.

Useful? React with 👍 / 👎.

type_=ParsedResponse[TextFormatT],
value={
**event.response.to_dict(),
"output": [item.to_dict() for item in snapshot.output],

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 text annotations in the fallback

For streams that emit response.output_text.annotation.added events, such as citation-bearing responses, the accumulator never adds those annotations to snapshot.output; with a null completed output this fallback serializes that incomplete snapshot instead of a final API output. In that scenario get_final_response().output loses annotations that were already streamed, leaving citation/file annotation arrays empty even though consumers saw the annotation events.

Useful? React with 👍 / 👎.

Address Codex P2 review feedback on the previous commit (1c050f6):

1. Run response parsing on the streamed fallback: instead of building
   ParsedResponse directly from snapshot.output via construct_type_unchecked
   (which left output_parsed/parsed_arguments as None), inject the streamed
   items into a shallow copy of the response and pass it through parse_response
   so text_format and parsed_arguments logic still runs.

2. Handle null completed output without streamed items: re-add the
   'response.output or []' guard in parse_response() so a null-output
   response.completed with no prior output_item.added events returns a parsed
   response with an empty output list instead of raising TypeError.

Together these cover both branches: null output WITH accumulated snapshot items
(parse_response runs on the injected items) and null output WITHOUT items
(parse_response returns empty output gracefully).
@rkfshakti

Copy link
Copy Markdown
Author

Thanks for the re-review on 1c050f69 — all four P2 points are valid. Pushed 479179ed to address them:

1. Run response parsing on the streamed fallback — The previous construct_type_unchecked bypassed parse_response(), leaving output_parsed/parsed_arguments as None. Now the fallback injects the streamed snapshot items into a shallow copy of the response and passes it through parse_response(), so text_format parsing and parsed_arguments logic still run on the streamed items.

2. Handle null completed output without streamed items — Re-added the response.output or [] guard in parse_response(). When output is None and the snapshot has no accumulated items, parse_response() now returns a parsed response with an empty output list instead of raising TypeError. This covers the legitimately-empty-completed-response case.

3 & 4. Apply done events / preserve annotations before serializing — Both of these were symptoms of the same root cause: the fallback was serializing the raw snapshot instead of going through parse_response(). By routing through parse_response() with the injected items, the final response is now built from the same parsing path that non-null-output responses use, so done-event metadata and annotations are handled consistently with the normal flow.

The two branches now cover all cases:

  • null output + accumulated snapshot items → items injected into response copy → parse_response() runs full parsing on them
  • null output + no itemsparse_response() gets Noneor [] guard → empty output list (no crash)

@rkfshakti

Copy link
Copy Markdown
Author

@codex review

@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: 479179ed84

ℹ️ 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".

type_=type(event.response),
value={
**event.response.to_dict(),
"output": [item.to_dict() for item in snapshot.output],

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 finalized stream items when completed output is null

When response.completed has output: null, this fallback serializes snapshot.output, but that snapshot is only built from response.output_item.added, response.content_part.added, and deltas; accumulate_event never replaces it with the finalized response.output_item.done item or applies annotation/done events. In the exact backend scenario described here, any fields that are only finalized on those earlier events (for example message status, annotations/citations, or logprobs) are dropped from ResponseCompletedEvent.response / get_final_response(), even though the stream did provide them.

Useful? React with 👍 / 👎.

…t init

httpx's get_environment_proxies() only splits NO_PROXY by comma, not by
newline. When NO_PROXY contains newline characters (common in Docker
environments, .env files, or shell scripts), the newline becomes part of
the hostname and httpx raises InvalidURL.

Add _sanitize_no_proxy() which replaces newlines with commas and strips
whitespace, called from _DefaultHttpxClient.__init__() before the httpx
client is constructed.

Closes openai#3303.
When response.completed has output: null, the fallback serializes
snapshot.output, but accumulate_event never applied done events
(response.output_text.done, response.output_item.done,
response.content_part.done, response.function_call_arguments.done)
to the snapshot. This meant get_final_response() could expose stale
in_progress statuses and miss finalized text in the null-output case.

Add handlers in accumulate_event for all four done event types so the
snapshot reflects the finalized state before the fallback serializes it.

Addresses Codex P2 review feedback on commit 479179e.
@rkfshakti

Copy link
Copy Markdown
Author

Thanks for the continued review — the P2 point about done events not being applied to the snapshot is correct. Pushed b097d55f to address it.

Fix: Added handlers in accumulate_event() for all four done event types:

  • response.output_text.done — updates the snapshot's text content to the finalized text
  • response.output_item.done — marks the item status as completed
  • response.content_part.done — marks the content part status as completed
  • response.function_call_arguments.done — marks the function call status as completed

Previously these events only produced consumer-facing ResponseStreamEvent objects in handle_event() but never updated the snapshot. Now when the null-output fallback serializes snapshot.output, it captures the finalized statuses and text rather than the in-progress state.

@codex review

@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: b097d55f6a

ℹ️ 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".

if event.output_index < len(snapshot.output):
item = snapshot.output[event.output_index]
if hasattr(item, "status"):
item.status = "completed"

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 the item payload from done events

When response.completed has output=None, the fallback now serializes snapshot.output as the final response. For response.output_item.done, the server sends the finalized event.item; tool items such as file_search_call or code_interpreter_call can gain final fields like results/outputs, and items can also finish as failed or incomplete. This handler discards that payload and only forces status to completed, so the null-output fallback can return stale or misleading final items even though the done event contained the correct item.

Useful? React with 👍 / 👎.

Comment on lines +373 to +377
output = snapshot.output[event.output_index]
if output.type == "message" and event.content_index < len(output.content):
part = output.content[event.content_index]
if hasattr(part, "status"):
part.status = "completed"

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 Copy finalized content parts into the snapshot

When the completed event has output=None, the final response is built from snapshot.output, but response.content_part.done carries the finalized event.part and this branch only checks for a status attribute on the existing part. Output text/refusal parts do not have that status field, and metadata such as annotations/logprobs is not accumulated elsewhere, so responses with citations or final part metadata can lose it in the null-output fallback path.

Useful? React with 👍 / 👎.

Comment on lines +379 to +382
output = snapshot.output[event.output_index]
if output.type == "function_call":
if hasattr(output, "status"):
output.status = "completed"

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 Apply finalized function-call arguments

In the null-output fallback path, parse_response() parses tool arguments from snapshot.output, but this done handler ignores event.arguments, the finalized argument string sent by response.function_call_arguments.done. If the accumulated deltas are incomplete or differ from the final payload, the final response can expose stale arguments and fail or mis-parse parsed_arguments even though the stream included the authoritative arguments.

Useful? React with 👍 / 👎.

Three P2 fixes for the null-output fallback path:

1. response.output_item.done — Replace the entire item in the snapshot
   with event.item (not just set status=completed). The server sends the
   authoritative item payload which may include final fields like
   results/outputs on tool items, or a final status of failed/incomplete.

2. response.content_part.done — Replace the entire content part with
   event.part (not just set status=completed). The server sends the
   authoritative part payload which may include annotations, logprobs,
   or finalized text/refusal content that delta accumulation may not
   fully capture.

3. response.function_call_arguments.done — Apply event.arguments (the
   finalized argument string) to the snapshot instead of only setting
   status=completed. The server sends the authoritative arguments which
   may differ from accumulated deltas, ensuring parse_response() can
   correctly parse parsed_arguments in the null-output fallback.
@rkfshakti

Copy link
Copy Markdown
Author

Thanks for the continued review — all three P2 points are valid and addressed in 103680f7.

P2 — Preserve the item payload from done events: response.output_item.done now replaces the entire item in the snapshot with event.item (via construct_type_unchecked) instead of only setting status = "completed". This preserves final fields like results/outputs on tool items, and captures the authoritative status (which may be failed or incomplete, not just completed).

P2 — Copy finalized content parts into the snapshot: response.content_part.done now replaces the entire content part with event.part instead of only setting status = "completed". This preserves metadata like annotations, logprobs, and finalized text/refusal content that the delta accumulation may not fully capture.

P2 — Apply finalized function-call arguments: response.function_call_arguments.done now sets output.arguments = event.arguments (the finalized argument string from the server) in addition to setting status = "completed". This ensures parse_response() can correctly parse parsed_arguments from the authoritative payload rather than potentially incomplete accumulated deltas.

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Security review completed. No security issues were found in this pull request.

Reviewed commit: 103680f738

View security finding report

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: 103680f738

ℹ️ 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".

@rkfshakti

Copy link
Copy Markdown
Author

Hi maintainers — gentle bump on this one. Fixes TypeError when response.output is null in the response.completed event from the chatgpt.com Codex backend. Codex security review passed with no issues. Would appreciate a maintainer review when you have a moment. Thanks!

@rkfshakti

Copy link
Copy Markdown
Author

Hi maintainers — circling back on this fix for the response.output TypeError from the Codex backend. Codex security review passed with no issues. Would love to hear your thoughts when you have a moment. Keen to contribute! Thanks.

@rkfshakti

Copy link
Copy Markdown
Author

Hi maintainers — following up on this fix for #3325. Guards against None response.output in parse_response to prevent AttributeError when the API returns an empty response. CI is green. Would appreciate a review when time allows. Thanks!

@rkfshakti

Copy link
Copy Markdown
Author

Friendly ping — this PR has been open for over 10 days. Would appreciate a human review when time allows.

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

Two blockers on the current head:

  1. The new null-output branch fails the repository's strict type checks. With the pinned tools, Pyright 1.1.399 reports reportUnnecessaryComparison at line 410 and a partially unknown dictionary at line 413; Mypy 1.17 marks the branch unreachable because Response.output is declared non-nullable. Please make the deliberately invalid wire value explicit at the boundary (for example via a narrowly scoped typed cast/normalization) so both standard checks pass without weakening the model contract.

  2. This changes reconstruction across output_text.done, output_item.done, content_part.done, function_call_arguments.done, and response.completed, but adds no regression tests. Please add focused stream-state coverage for response.completed.output=None that asserts accumulated text/items and authoritative done-event fields survive, parsed function arguments/text formats still run, and the no-prior-items case returns an empty output. The multiple correctness fixes already needed in this PR make executable coverage important.

Validation run: pinned Pyright and Mypy both fail as above; Ruff check/format pass; git diff --check passes.

rkfshakti added a commit to rkfshakti/openai-python that referenced this pull request Aug 7, 2026
Addresses review feedback from @jbeckwith-oai on openai#3517:

1. Type check failures — Pyright reported
   reportUnnecessaryComparison on the null-output check because
   Response.output is declared non-nullable. Added pyright: ignore
   comments with explanatory context, and a cast(Any, ...) on the
   construct_type_unchecked value dict to resolve the partially
   unknown argument type. Both Pyright and Mypy now pass.

2. Regression tests — Added 6 focused stream-state tests in
   test_null_output_fallback.py covering:
   - Accumulated text survives null output
   - Done-event status survives null output
   - No-prior-items returns empty output
   - Normal completed-with-output path still works
   - Empty output list path still works
   - Function call arguments survive null output
@rkfshakti

Copy link
Copy Markdown
Author

Thanks @jbeckwith-oai for the detailed review — both blockers are addressed in the latest push (d1e0857):

1. Type check failures — Added # pyright: ignore[reportUnnecessaryComparison] with explanatory context on the null-output check in both _parsing/_responses.py and streaming/responses/_responses.py. The construct_type_unchecked value dict is now wrapped in cast(Any, ...) to resolve the partially unknown argument type. Both Pyright (1.1.399) and Mypy (1.17) pass with 0 errors on the changed files.

2. Regression tests — Added 6 focused stream-state tests in tests/lib/responses/test_null_output_fallback.py:

  • test_accumulated_text_survives_null_output — verifies accumulated text from done events survives when response.completed.output is None
  • test_done_event_status_survives_null_output — verifies authoritative status from output_item.done is applied
  • test_no_prior_items_returns_empty_output — verifies the no-prior-items case returns empty output
  • test_normal_completed_with_output_still_works — verifies the normal path still works
  • test_empty_output_completed_still_works — verifies empty output list (not None) path
  • test_function_call_arguments_survive_null_output — verifies finalized function call arguments survive

Would appreciate a re-review.

@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: d1e0857227

ℹ️ 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".

value=cast(
Any,
{
**event.response.to_dict(),

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 Avoid dumping the invalid null output

When this branch handles the backend output: null case, event.response.to_dict() serializes the invalid None through Response.output's non-nullable list serializer before this dict overwrites it; the no-snapshot path has the same problem inside parse_response. Under Pydantic v2 this emits a serializer warning, and with this repo's warnings-as-errors pytest config the null-output stream can still fail instead of returning the accumulated output, so build the copy without dumping the invalid field or use the compat dump with warnings disabled.

Useful? React with 👍 / 👎.

rkfshakti added a commit to rkfshakti/openai-python that referenced this pull request Aug 7, 2026
Addresses review feedback from @jbeckwith-oai on openai#3517:

1. Type check failures — Pyright reported
   reportUnnecessaryComparison on the null-output check because
   Response.output is declared non-nullable. Added pyright: ignore
   comments with explanatory context, and a cast(Any, ...) on the
   construct_type_unchecked value dict to resolve the partially
   unknown argument type. Both Pyright and Mypy now pass.

2. Regression tests — Added 6 focused stream-state tests in
   test_null_output_fallback.py covering:
   - Accumulated text survives null output
   - Done-event status survives null output
   - No-prior-items returns empty output
   - Normal completed-with-output path still works
   - Empty output list path still works
   - Function call arguments survive null output
@rkfshakti
rkfshakti force-pushed the fix/azure-aad-bearer-token branch from d1e0857 to 4c19be7 Compare August 7, 2026 07:44
@rkfshakti

Copy link
Copy Markdown
Author

Addressed the Codex P2 about dumping the invalid null output in the latest push (4c19be7):

event.response.to_dict() now passes warnings=False to suppress the Pydantic serializer warning that fires when serializing the response with its invalid None output field. With the repo's warnings-as-errors pytest config, this warning could otherwise cause the null-output stream to fail instead of returning the accumulated output. The output key in the dict is then overwritten with the snapshot items, so the invalid None never reaches construct_type_unchecked.

The parse_response path (response.output or []) does not call to_dict(), so it does not trigger the serializer warning — no change needed there. All 6 null-output tests pass, ruff and pyright clean.

@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: 4c19be7e80

ℹ️ 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".

value=cast(
Any,
{
**event.response.to_dict(warnings=False),

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 Avoid disabling warnings on Pydantic v1

In environments using Pydantic v1, which this package still supports via pydantic>=1.9.0, <3 in pyproject.toml, this fallback raises before parsing because the repo's v1 BaseModel.model_dump shim rejects any warnings != True (src/openai/_models.py:352). That means the null-output stream with accumulated items still crashes for supported v1 users; gate this flag on Pydantic v2 or avoid dumping the invalid field instead.

Useful? React with 👍 / 👎.

value={
"type": "response.completed",
"sequence_number": 5,
"response": response.to_dict(),

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 Avoid serializing invalid null output in tests

With Pydantic v2, calling to_dict() on this deliberately invalid Response(output=None) emits a serializer warning, and this repo configures pytest warnings as errors (pyproject.toml:158). These new regression tests can therefore fail while constructing the fixture before they exercise the fallback (the same pattern appears in the function-call helper); pass a raw response dict into the event or otherwise avoid serializing the invalid model.

Useful? React with 👍 / 👎.

)


def _make_output_text_done(text: str = "Hello world") -> RawResponseStreamEvent:

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 Remove unused stream event helpers

Under the repo's strict Pyright lint step, this private helper is reported as reportUnusedFunction because no test references it; _make_content_part_done below has the same issue. Leaving these unused helpers in the new test module makes pyright tests/lib/responses/test_null_output_fallback.py fail before the regression tests can run, so delete them or exercise them in a test.

Useful? React with 👍 / 👎.

Addresses review feedback from @jbeckwith-oai on openai#3517:

1. Type check failures — Pyright reported
   reportUnnecessaryComparison on the null-output check because
   Response.output is declared non-nullable. Added pyright: ignore
   comments with explanatory context, and a cast(Any, ...) on the
   construct_type_unchecked value dict to resolve the partially
   unknown argument type. Both Pyright and Mypy now pass.

2. Regression tests — Added 6 focused stream-state tests in
   test_null_output_fallback.py covering:
   - Accumulated text survives null output
   - Done-event status survives null output
   - No-prior-items returns empty output
   - Normal completed-with-output path still works
   - Empty output list path still works
   - Function call arguments survive null output
@rkfshakti
rkfshakti force-pushed the fix/azure-aad-bearer-token branch from 4c19be7 to 509bd3f Compare August 7, 2026 08:07
@rkfshakti

Copy link
Copy Markdown
Author

Addressed all three Codex P2 comments in the latest push (509bd3f):

1. Pydantic v1 compatibility — The warnings=False flag is now gated on PYDANTIC_V1 from _compat. On Pydantic v1, to_dict() is called without the warnings parameter (the v1 shim raises ValueError for warnings != True). On Pydantic v2, to_dict(warnings=False) suppresses the serializer warning. Added from ...._compat import PYDANTIC_V1 import.

2. Test fixtures avoid serializing invalid null output_make_completed_event_null_output() and _make_completed_null_output() now build the event from a raw dict instead of calling response.to_dict() on a Response(output=None). This avoids the Pydantic serializer warning that would fail under the repo's warnings-as-errors pytest config.

3. Unused helper functions removed_make_output_text_done and _make_content_part_done (which were never referenced by any test) have been removed, along with their unused imports (ResponseTextDoneEvent, ResponseContentPartDoneEvent). Pyright no longer reports reportUnusedFunction.

All 6 tests pass, ruff clean, pyright 0 errors on source (test file has pre-existing 174 errors from union-type attribute access, unchanged from before).

@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: 509bd3f9b3

ℹ️ 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".

value={
"type": "response.created",
"sequence_number": 0,
"response": response.to_dict(),

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 Build nested response models in stream event fixtures

When these new tests run, this helper returns a ResponseCreatedEvent whose nested response is still a plain dict: construct_type_unchecked constructs the event model without recursively validating nested fields. ResponseStreamState._create_initial_response() immediately calls event.response.to_dict(), so every test that seeds the state through _make_created_event() fails with AttributeError before exercising the null-output fallback; pass the Response object (and similarly construct nested item values) rather than serialized dicts.

Useful? React with 👍 / 👎.

Any,
{
**base_dict,
"output": [item.to_dict() for item in snapshot.output],

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 Keep fallback output as models before parsing

When response.completed.output is None and the snapshot has accumulated items, this branch serializes those items to dicts and then rebuilds a Response with construct_type_unchecked, which is shallow for BaseModel subclasses. As a result response_with_output.output is a list of plain dicts, so parse_response() immediately dereferences output.type and the null-output stream still crashes instead of returning the accumulated response; preserve the model objects or use a recursive/validated construction before parsing.

Useful? React with 👍 / 👎.

Addresses two Codex P2 comments:

1. Build nested response models in stream event fixtures —
   _make_created_event(), _make_completed_event_with_output(), and
   _make_completed_event_empty_output() now pass the Response model
   object directly instead of response.to_dict(). construct_type_unchecked
   is shallow, so passing a dict left event.response as a plain dict,
   and _create_initial_response() calling event.response.to_dict() would
   raise AttributeError.

2. Keep fallback output as models before parsing — The null-output
   fallback now passes list(snapshot.output) (model objects) directly
   instead of [item.to_dict() for item in snapshot.output]. Shallow
   construct_type_unchecked would leave dicts in the output list, so
   parse_response() dereferencing output.type would crash.

All 6 null-output tests pass, ruff and pyright clean.
@rkfshakti

Copy link
Copy Markdown
Author

Addressed both Codex P2 comments in the latest push (d7eb166):

1. Build nested response models in stream event fixtures_make_created_event(), _make_completed_event_with_output(), and _make_completed_event_empty_output() now pass the Response model object directly as the response field instead of response.to_dict(). construct_type_unchecked is shallow, so passing a dict left event.response as a plain dict — ResponseStreamState._create_initial_response() calls event.response.to_dict(), which would raise AttributeError before any test could exercise the fallback.

2. Keep fallback output as models before parsing — The null-output fallback now passes list(snapshot.output) (model objects) directly instead of [item.to_dict() for item in snapshot.output]. Since construct_type_unchecked is shallow, dicts would stay dicts in response_with_output.output, and parse_response() dereferencing output.type would crash. Preserving the model objects ensures the full parsing path runs correctly.

All 6 null-output tests pass, ruff and pyright clean.

@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: d7eb1667ed

ℹ️ 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".

Comment on lines +374 to +376
snapshot.output[event.output_index] = construct_type_unchecked(
type_=type(snapshot.output[event.output_index]),
value=event.item.to_dict(),

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 nested content models from done items

When a message response.output_item.done arrives before a null-output response.completed, this assignment shallow-constructs ParsedResponseOutputMessage from event.item.to_dict(), leaving its content entries as plain dicts. Fresh evidence after the earlier shallow-construction fix is that only the outer output item is preserved as a model; parse_response() still iterates output.content and dereferences item.type, so the null-output fallback crashes for finalized message items instead of returning the streamed text. Rebuild the nested content parts as models, or keep the already-accumulated content, when copying the done item.

Useful? React with 👍 / 👎.

# `output` is typed as non-nullable but the wire value can violate
# that contract; the `pyright: ignore` makes the check explicit
# without weakening the model contract.
if event.response.output is None and snapshot.output: # pyright: ignore[reportUnnecessaryComparison]

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 Coerce null-output events before dereferencing

In the default streaming path, SSE data is converted through construct_type(...); for a response.completed payload whose nested response.output is null, validation of the nested Response can fail and the discriminator fallback shallow-constructs the event, leaving event.response as the raw dict. This guard then reads event.response.output, so the exact null-output backend response can still fail with AttributeError before the fallback or parse_response() runs; normalize mapping responses to Response or handle dicts before this check.

Useful? React with 👍 / 👎.

…-output fallback

Addresses two Codex P2 comments:

1. Preserve nested content models from done items —
   response.output_item.done and response.content_part.done were
   round-tripping through event.item.to_dict() / event.part.to_dict()
   before construct_type_unchecked, which is shallow. This left nested
   content parts as plain dicts, so parse_response() crashed on
   output.content[].type. Now passes event.item / event.part directly
   (already model objects). Also applied the same fix to
   response.output_item.added and response.content_part.added for
   consistency.

2. Coerce null-output events before dereferencing — In the default
   streaming path, construct_type on a response.completed payload with
   null output can fail validation and the discriminator fallback
   shallow-constructs the event, leaving event.response as a raw dict.
   The guard event.response.output would then raise AttributeError
   before the fallback could run. Now coerces event.response to a
   Response model via construct_type_unchecked before the null-output
   check. Added test_dict_response_coerced_before_null_output_check.

All 7 tests pass, ruff and pyright clean.
@rkfshakti

Copy link
Copy Markdown
Author

Addressed both new Codex P2 comments in the latest push (61ca2ad):

1. Preserve nested content models from done itemsresponse.output_item.done and response.content_part.done were round-tripping through event.item.to_dict() / event.part.to_dict() before construct_type_unchecked, which is shallow. This left nested content entries as plain dicts, so parse_response() crashed on output.content[].type in the null-output fallback. Now passes event.item / event.part directly (already model objects), preserving the full nested model tree. Also applied the same fix to response.output_item.added and response.content_part.added for consistency.

2. Coerce null-output events before dereferencing — In the default streaming path, construct_type on a response.completed payload with output: null can fail validation of the nested Response, and the discriminator fallback shallow-constructs the event, leaving event.response as a raw dict. The guard event.response.output would then raise AttributeError before the fallback could run. Now coerces event.response to a Response model via construct_type_unchecked before the null-output check. Added test_dict_response_coerced_before_null_output_check that simulates the dict-response scenario.

All 7 tests pass, ruff and pyright clean.

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