refactor(codex): drop SDK responses.stream() helper; consume events directly - #33042
Merged
Conversation
…irectly The OpenAI Python SDK's high-level `client.responses.stream(...)` helper does post-hoc typed reconstruction from the terminal `response.completed.response.output` field. The chatgpt.com Codex backend has been observed (today, gpt-5.5) to ship `response.output = null` on terminal frames, which crashes the SDK with `TypeError: 'NoneType' object is not iterable` mid-iteration. Carlton's #32963 patched the symptom by wrapping the helper in try/except and recovering from the same per-event accumulator the SDK was supposed to populate. This PR removes the helper from the call path entirely: we now use `client.responses.create(stream=True)` (raw AsyncIterable of SSE events) and assemble the final response object ourselves from `response.output_item.done` events as they arrive. The terminal event's `output` field is never read for content. Same strategy OpenClaw uses for the same backend. This makes Hermes structurally immune to the bug class, not patched. The next time OpenAI ships a shape change to chatgpt.com's terminal frame, our consumer keeps working because it doesn't read that frame for content — only for usage/status/id. Changes - `agent/codex_runtime.py`: new `_consume_codex_event_stream()` shared consumer; `run_codex_stream()` uses `responses.create(stream=True)`; `run_codex_create_stream_fallback()` collapses into a thin alias since the primary path now does what the fallback used to do. - `agent/auxiliary_client.py`: `_CodexCompletionsAdapter` uses the same consumer; old null-output recovery helpers deleted as unreferenced. - Tests migrated: fixtures that mocked `responses.stream` now mock `responses.create` returning a raw iterable. New regression test asserts the auxiliary path returns streamed items even when the terminal event's `output` is literally `null`. Validation - Live: tested against fresh OAuth on `chatgpt.com/backend-api/codex` with `gpt-5.5` — response built correctly with `response.output=null` on the terminal frame, all events consumed, usage/reasoning tokens propagated. - `tests/run_agent/test_run_agent_codex_responses.py` + `tests/agent/test_auxiliary_client.py`: 242 passed.
Contributor
🔎 Lint report:
|
| Rule | Count |
|---|---|
invalid-assignment |
2 |
invalid-argument-type |
2 |
unresolved-import |
1 |
First entries
agent/codex_runtime.py:279: [invalid-assignment] invalid-assignment: Object of type `None` is not assignable to `str`
agent/codex_runtime.py:482: [invalid-argument-type] invalid-argument-type: Argument to function `_consume_codex_event_stream` is incorrect: Expected `str`, found `Unknown | None`
agent/codex_runtime.py:358: [invalid-assignment] invalid-assignment: Object of type `Any | None` is not assignable to `str`
agent/auxiliary_client.py:818: [invalid-argument-type] invalid-argument-type: Argument to function `_consume_codex_event_stream` is incorrect: Expected `str`, found `Any | None`
tests/run_agent/test_codex_xai_oauth_recovery.py:112: [unresolved-import] unresolved-import: Cannot resolve imported module `httpx`
✅ Fixed issues (4):
| Rule | Count |
|---|---|
invalid-argument-type |
2 |
invalid-parameter-default |
1 |
invalid-type-form |
1 |
First entries
agent/codex_runtime.py:185: [invalid-parameter-default] invalid-parameter-default: Default value of type `None` is not assignable to annotated parameter type `str`
agent/codex_runtime.py:323: [invalid-argument-type] invalid-argument-type: Argument to function `_codex_backfilled_response` is incorrect: Expected `str`, found `Unknown | None`
agent/auxiliary_client.py:871: [invalid-argument-type] invalid-argument-type: Argument to function `_responses_backfilled_response` is incorrect: Expected `str`, found `Any | None`
agent/codex_runtime.py:210: [invalid-type-form] invalid-type-form: Function `callable` is not valid in a parameter annotation: Did you mean `collections.abc.Callable`?
Unchanged: 4969 pre-existing issues carried over.
Diagnostics are surfaced as warnings — this check never fails the build.
CI surfaced 10 test failures across tests/run_agent/test_streaming.py and tests/run_agent/test_codex_xai_oauth_recovery.py — both files had their own `responses.stream(...)` mocks I missed in the first sweep. agent/codex_runtime.py: _consume_codex_event_stream() now raises "Codex Responses stream did not emit a terminal response" when the stream ends without any terminal frame AND no usable content. This preserves the signal callers used to get from the SDK's high-level helper, which they distinguished from "completed with empty body" in error handling. Tests migrated: - test_streaming.py: text-delta callback, activity-touch, and remote-protocol-error tests all switch from mocking responses.stream to responses.create returning an iterable of events. - test_codex_xai_oauth_recovery.py: prelude-error tests are recast as wire-error-event tests (the new path raises _StreamErrorEvent directly when the wire emits type=error, which is strictly better than the old two-phase "SDK RuntimeError → retry → fallback"). The retry-on-transport-error test moves from responses.stream side-effect to responses.create side-effect. Verified live against chatgpt.com Codex with gpt-5.5 — AIAgent.chat() through the full codex_responses path returns correctly, 319/319 targeted tests passing.
This was referenced May 27, 2026
NoneType object is not iterable crash when Codex Responses API returns response.output = None
#32991
Closed
alt-glitch
pushed a commit
that referenced
this pull request
Jun 14, 2026
…registered Salvages the transport-side fix from #32911 (@xxxigm). Closes #32892. The openai SDK's responses.stream() / responses.parse() eagerly call _make_tools(tools), which iterates tools without a None guard. Passing tools=None raises TypeError: 'NoneType' object is not iterable before any HTTP request is issued (openai==2.24.0). PR #33042 already removed responses.stream() from our own Codex call paths, so the specific iteration crash inside _make_tools is no longer on the hot path. But the right API contract is to omit tools entirely when there are no functions to expose — passing tools=None to the backend is semantically wrong regardless of the SDK's iteration behavior, and we'd hit it again on any future code path that hasn't migrated off responses.stream(). This applies the transport-level part of @xxxigm's fix: move 'tools': response_tools into the if response_tools: branch so the key is omitted when there are no tools, just like tool_choice and parallel_tool_calls already are. Skips the run_agent.py-side _strip_sdk_none_iterables helper from their PR — that path is now obsolete because the SDK helper that needed defending is gone. Tests - tests/run_agent/test_codex_no_tools_nonetype.py: 6 tests trimmed from @xxxigm's original 13-test file. Drops the obsolete tests for _strip_sdk_none_iterables and _RecordingResponsesStream (helpers that don't exist on main anymore), keeps the transport behavior tests + the SDK contract sanity check that ensures we notice if upstream ever fixes _make_tools(None). - 6/6 passing locally. Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…irectly (NousResearch#33042) * refactor(codex): drop SDK responses.stream() helper; consume events directly The OpenAI Python SDK's high-level `client.responses.stream(...)` helper does post-hoc typed reconstruction from the terminal `response.completed.response.output` field. The chatgpt.com Codex backend has been observed (today, gpt-5.5) to ship `response.output = null` on terminal frames, which crashes the SDK with `TypeError: 'NoneType' object is not iterable` mid-iteration. Carlton's NousResearch#32963 patched the symptom by wrapping the helper in try/except and recovering from the same per-event accumulator the SDK was supposed to populate. This PR removes the helper from the call path entirely: we now use `client.responses.create(stream=True)` (raw AsyncIterable of SSE events) and assemble the final response object ourselves from `response.output_item.done` events as they arrive. The terminal event's `output` field is never read for content. Same strategy OpenClaw uses for the same backend. This makes Hermes structurally immune to the bug class, not patched. The next time OpenAI ships a shape change to chatgpt.com's terminal frame, our consumer keeps working because it doesn't read that frame for content — only for usage/status/id. Changes - `agent/codex_runtime.py`: new `_consume_codex_event_stream()` shared consumer; `run_codex_stream()` uses `responses.create(stream=True)`; `run_codex_create_stream_fallback()` collapses into a thin alias since the primary path now does what the fallback used to do. - `agent/auxiliary_client.py`: `_CodexCompletionsAdapter` uses the same consumer; old null-output recovery helpers deleted as unreferenced. - Tests migrated: fixtures that mocked `responses.stream` now mock `responses.create` returning a raw iterable. New regression test asserts the auxiliary path returns streamed items even when the terminal event's `output` is literally `null`. Validation - Live: tested against fresh OAuth on `chatgpt.com/backend-api/codex` with `gpt-5.5` — response built correctly with `response.output=null` on the terminal frame, all events consumed, usage/reasoning tokens propagated. - `tests/run_agent/test_run_agent_codex_responses.py` + `tests/agent/test_auxiliary_client.py`: 242 passed. * test+fix(codex): migrate streaming tests, raise on truncated streams CI surfaced 10 test failures across tests/run_agent/test_streaming.py and tests/run_agent/test_codex_xai_oauth_recovery.py — both files had their own `responses.stream(...)` mocks I missed in the first sweep. agent/codex_runtime.py: _consume_codex_event_stream() now raises "Codex Responses stream did not emit a terminal response" when the stream ends without any terminal frame AND no usable content. This preserves the signal callers used to get from the SDK's high-level helper, which they distinguished from "completed with empty body" in error handling. Tests migrated: - test_streaming.py: text-delta callback, activity-touch, and remote-protocol-error tests all switch from mocking responses.stream to responses.create returning an iterable of events. - test_codex_xai_oauth_recovery.py: prelude-error tests are recast as wire-error-event tests (the new path raises _StreamErrorEvent directly when the wire emits type=error, which is strictly better than the old two-phase "SDK RuntimeError → retry → fallback"). The retry-on-transport-error test moves from responses.stream side-effect to responses.create side-effect. Verified live against chatgpt.com Codex with gpt-5.5 — AIAgent.chat() through the full codex_responses path returns correctly, 319/319 targeted tests passing.
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…dapter Closes NousResearch#33368. `_CodexCompletionsAdapter.create()` iterates `final.output` from the Codex Responses stream. The event-driven consumer (introduced in NousResearch#33042) always sets `final.output` to a list, so this shape can't come from our own code path. But: - Mocked clients in tests can return a typed Response with `output=None` - Third-party shims / compatibility layers that bypass the consumer can do the same - A future code path that wraps a different consumer could regress The old code `getattr(final, "output", [])` returns `None` (not the default `[]`) when the attribute EXISTS but is `None`. Iterating `None` then raises `TypeError: 'NoneType' object is not iterable` — the exact error logged by title-generation when this fires. Fix: `getattr(final, "output", None) or []` — single-line defensive coerce. Cheap; zero risk. Regression test asserts the auxiliary path handles a final whose `.output` is `None` (via monkey-patched consumer) without raising and returns the expected chat.completions-shaped response. Reporter: @pavegrid-1 (issue NousResearch#33368).
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…ble provider shape error Salvages the intent of NousResearch#33136 (@Brixyy) onto current main. The original PR was written against the pre-refactor monolithic run_agent.py and added a top-level _is_nonretryable_local_validation_error() helper. Both target functions have since been extracted to agent/conversation_loop.py:2869, so the salvage applies the equivalent guard inline at that canonical location rather than reintroducing the helper. ## Why After NousResearch#33042 made our own Codex consumer structurally immune to NoneType crashes, third-party shims, mocked clients, and any future code path that hasn't migrated could still surface TypeError: 'NoneType' object is not iterable as a wire-shape mismatch. The agent loop's classifier currently treats ALL TypeError as a local programming bug and aborts non-retryable — users on stale Telegram/gateway turns saw bare "Non-retryable error (HTTP None)" with no recovery. This is a provider/SDK shape mismatch, not a local programming bug. The retry/fallback path should run, not be short-circuited. ## What agent/conversation_loop.py: extend is_local_validation_error to exclude TypeErrors whose message matches the NoneType-not-iterable shape (case- insensitive, both "NoneType" and "not iterable" must appear). tests/run_agent/test_jsondecodeerror_retryable.py: - update the mirror predicate to match the production check - add TestNoneTypeNotIterableIsRetryable class with 3 tests (the basic shape, message variants, unrelated TypeErrors still abort) - add TestAgentLoopSourceHasNoneTypeCarveOut to enforce the source-level invariant matches the test mirror ## Validation tests/run_agent/test_jsondecodeerror_retryable.py + tests/run_agent/test_31273_402_not_retried.py → 14/14 passing Co-authored-by: Brixyy <subrtt@gmail.com>
T02200059
pushed a commit
to T02200059/hermes-agent
that referenced
this pull request
Jun 18, 2026
…registered Salvages the transport-side fix from NousResearch#32911 (@xxxigm). Closes NousResearch#32892. The openai SDK's responses.stream() / responses.parse() eagerly call _make_tools(tools), which iterates tools without a None guard. Passing tools=None raises TypeError: 'NoneType' object is not iterable before any HTTP request is issued (openai==2.24.0). PR NousResearch#33042 already removed responses.stream() from our own Codex call paths, so the specific iteration crash inside _make_tools is no longer on the hot path. But the right API contract is to omit tools entirely when there are no functions to expose — passing tools=None to the backend is semantically wrong regardless of the SDK's iteration behavior, and we'd hit it again on any future code path that hasn't migrated off responses.stream(). This applies the transport-level part of @xxxigm's fix: move 'tools': response_tools into the if response_tools: branch so the key is omitted when there are no tools, just like tool_choice and parallel_tool_calls already are. Skips the run_agent.py-side _strip_sdk_none_iterables helper from their PR — that path is now obsolete because the SDK helper that needed defending is gone. Tests - tests/run_agent/test_codex_no_tools_nonetype.py: 6 tests trimmed from @xxxigm's original 13-test file. Drops the obsolete tests for _strip_sdk_none_iterables and _RecordingResponsesStream (helpers that don't exist on main anymore), keeps the transport behavior tests + the SDK contract sanity check that ensures we notice if upstream ever fixes _make_tools(None). - 6/6 passing locally. Co-authored-by: xxxigm <tuancanhnguyen706@gmail.com>
xyshanren
pushed a commit
to xyshanren/hermes-agent-cn
that referenced
this pull request
Jun 25, 2026
…dapter Closes NousResearch#33368. `_CodexCompletionsAdapter.create()` iterates `final.output` from the Codex Responses stream. The event-driven consumer (introduced in NousResearch#33042) always sets `final.output` to a list, so this shape can't come from our own code path. But: - Mocked clients in tests can return a typed Response with `output=None` - Third-party shims / compatibility layers that bypass the consumer can do the same - A future code path that wraps a different consumer could regress The old code `getattr(final, "output", [])` returns `None` (not the default `[]`) when the attribute EXISTS but is `None`. Iterating `None` then raises `TypeError: 'NoneType' object is not iterable` — the exact error logged by title-generation when this fires. Fix: `getattr(final, "output", None) or []` — single-line defensive coerce. Cheap; zero risk. Regression test asserts the auxiliary path handles a final whose `.output` is `None` (via monkey-patched consumer) without raising and returns the expected chat.completions-shaped response. Reporter: @pavegrid-1 (issue NousResearch#33368).
This was referenced Jun 30, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The OpenAI SDK's high-level
client.responses.stream(...)helper does post-hoc typed reconstruction fromresponse.completed.response.output. The chatgpt.com Codex backend (gpt-5.5, today) returns that field asnull, crashing the SDK withTypeError: 'NoneType' object is not iterablemid-iteration.#32963 patched the symptom. This removes the helper from the call path entirely —
responses.create(stream=True)raw event iteration + per-event reconstruction. Same strategy OpenClaw uses for the same backend. Bug class is now structurally impossible, not patched.Closes the recurrence vector for the May 26 outage (issue #11179) and the (today, gpt-5.5) recurrence pattern.
Changes
agent/codex_runtime.py: new_consume_codex_event_stream()shared consumer.run_codex_stream()usesresponses.create(stream=True)directly.run_codex_create_stream_fallback()is now a thin alias — the primary IS the fallback shape.agent/auxiliary_client.py:_CodexCompletionsAdapteruses the same consumer. Old null-output recovery helpers (_responses_null_output_iterable_error,_responses_backfilled_response) removed as unreferenced.responses.streammocks →responses.createiterables. New regression test (test_recovers_output_item_when_terminal_event_has_null_output) asserts the auxiliary path returns streamed items even when the terminal frame'soutputis literallynull.Validation
Live test against fresh OpenAI Codex OAuth, gpt-5.5 on chatgpt.com/backend-api/codex:
response.completed.response.output = nullis current steady-state, not transientresponse.created,in_progress,output_item.added,output_item.done,content_part.added,output_text.delta,output_text.done,content_part.done,completed)Tests:
tests/run_agent/test_run_agent_codex_responses.py+tests/agent/test_auxiliary_client.py→ 242 passed.responses.stream()callsget_final_response()callsTypeError-91Infographic