Skip to content

fix: tolerate null response output when parsing responses#3316

Open
jin-castle wants to merge 1 commit into
openai:mainfrom
jin-castle:fix/responses-null-output-parse
Open

fix: tolerate null response output when parsing responses#3316
jin-castle wants to merge 1 commit into
openai:mainfrom
jin-castle:fix/responses-null-output-parse

Conversation

@jin-castle
Copy link
Copy Markdown

Summary

Handle Responses objects whose output is None when parsing Responses API results.

A live downstream regression in Hermes/Codex surfaced this path: the backend can stream usable output events, then the terminal Responses object may deserialize with response.output is None. parse_response() currently iterates response.output directly and raises TypeError: 'NoneType' object is not iterable before callers can recover from streamed events.

This keeps parsing defensive by treating null output like an empty output list.

Related downstream report: NousResearch/hermes-agent#32883

Tests

python -m pytest tests\lib\responses\test_responses.py -q -o addopts=
6 passed in 1.28s

Also ran:

python -m ruff check src\openai\lib\_parsing\_responses.py tests\lib\responses\test_responses.py
python -m ruff format --check src\openai\lib\_parsing\_responses.py tests\lib\responses\test_responses.py

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: f310209b62

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

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.

P1 Badge Preserve streamed output when terminal output is null

When a streamed response has already accumulated response.output_item.* / text events but the terminal response.completed payload has response.output is None, this fallback causes parse_response() to return a completed ParsedResponse with output=[]. I checked the streaming path in src/openai/lib/streaming/responses/_responses.py: ResponseStreamState.accumulate_event() assigns _completed_response = parse_response(... response=event.response ...), and get_final_response() returns that object rather than the accumulated snapshot, so callers lose all streamed content instead of just avoiding the TypeError.

Useful? React with 👍 / 👎.

@iqdoctor
Copy link
Copy Markdown

Thanks for jumping on this. This looks like a duplicate of the earlier upstream fix PR:

#3315

Your PR covers the parse_response() side of the same response.output is None failure. #3315 already includes that guard, adds a matching regression test, and also covers the related Response.output_text accessor path so consumers do not hit the same raw TypeError: 'NoneType' object is not iterable through the convenience property.

I compared the diffs and did not see an additional behavior change here that needs to be pulled into #3315.

@iqdoctor
Copy link
Copy Markdown

Update: the automated Codex review here identified a useful stream-state data-loss risk. I pulled that into the earlier PR #3315.

#3315 now preserves the accumulated ResponseStreamState snapshot when the terminal completed response has output=None or output=[], and adds regression coverage for both cases after streamed text deltas.

So this PR remains a duplicate of #3315, but the useful review finding from this thread is now incorporated there.

@keenwind85
Copy link
Copy Markdown

Confirming production impact: this regression broke every openai-codex provider call in our Hermes deployment today between 00:02 and 03:04 UTC — the Slack mention bot returned 'NoneType' object is not iterable to every user message, and our cron PR-review job failed ~12 times in a row. The terminal Responses object arriving with response.output is None matches your description exactly.

Traceback for reference:

File "openai/lib/streaming/responses/_responses.py", line 360, in accumulate_event
    self._completed_response = parse_response(
File "openai/lib/_parsing/_responses.py", line 61, in parse_response
    for output in response.output:
TypeError: 'NoneType' object is not iterable

Applied the one-line guard (response.output or []) locally and verified gpt-5.5 streaming via the Codex backend completes cleanly through the downstream stream consumer. The test you added is the right shape — would love to see this land.

@devnull37
Copy link
Copy Markdown

Independent confirmation from the Hermes/Codex side — this or [] guard is the correct minimal fix.

Reproduced identically against https://chatgpt.com/backend-api/codex with gpt-5.5 on openai==2.24.0; same traceback at lib/_parsing/_responses.py:61 via responses.stream()accumulate_eventparse_response() on the response.completed event.

Two extra data points that may help confirm this is the right layer to fix:

  • The model is valid — not the cause. Probing GET /backend-api/codex/models?client_version=1.0.0 lists gpt-5.5 with "supported_in_api": true. The crash is purely the terminal response.completed carrying output=null, not an invalid-model artifact.
  • The content is on the wire; it's just absent from the terminal object. On this backend the output items arrive as preceding response.output_item.done events (in my repro: 2 items — a reasoning item + the message), and text via response.output_text.delta/.done. So once parse_response stops raising, get_final_response() returns output=[] and the caller can backfill the assistant turn from the streamed items. This guard is what unblocks that recovery path — not merely crash suppression.

Verified end-to-end: applying exactly this one-liner makes responses.stream() return cleanly and the downstream agent produces a normal response. The change + test_parse_response_allows_null_output look complete. 👍

@ohrbot613
Copy link
Copy Markdown

+1, independently reproduced this against the live https://chatgpt.com/backend-api/codex/responses endpoint with openai==2.38.0.

The Codex backend can complete a stream with reasoning-only / tool-call-only output, after which the terminal Response deserializes with output: null. parse_response() then hits for output in response.output: at line 61 → TypeError: 'NoneType' object is not iterable. This breaks every retry attempt since the exception classifies as non-retryable.

Workaround I'm running locally while this lands: a runtime _hermes_openai_patches.pth shim that monkey-patches parse_response to coerce None → []. Surviving pip install openai was the only way to keep hermes/Tom alive across reinstalls. Happy to drop it the moment this merges.

Diff in this PR matches what I patched directly in _responses.py:61. LGTM.

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.

5 participants