hermes - 💡(How to fix) Fix [Bug]: Codex Responses stream ending without `response.completed` crashes with TypeError ('NoneType' object is not iterable) — bypasses existing backfill at codex_runtime.py:265-280

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

venv/lib/python3.11/site-packages/openai/lib/_parsing/_responses.py:61

for output in response.output: TypeError: 'NoneType' object is not iterable

Root Cause

Hermes already has dedicated backfill logic for this exact case at agent/codex_runtime.py:265-280 (with explanatory comments referencing the chatgpt.com backend) — but it never executes, because get_final_response() raises before control reaches the backfill block. The existing exception handler at codex_runtime.py:302 only catches RuntimeError (the older Expected to have received \response.completed`shape from earlier openai SDK versions), so the newerTypeError` shape jumps past every recovery path and is escalated as a non-retryable client error.

Fix Action

Fix / Workaround

The official codex CLI tolerates this (works against the same OAuth account/network/account-ID without crashing), so the workaround belongs on the client side.

final_response = stream.get_final_response()        # ← raises TypeError, never returns
# PATCH: ChatGPT Codex backend streams valid output items
# but get_final_response() can return an empty output list.
# Backfill from collected items or synthesize from deltas.
_out = getattr(final_response, "output", None)
if isinstance(_out, list) and not _out:
    if collected_output_items:
        final_response.output = list(collected_output_items)
    ...

This treats any leaked TypeError as a "local programming bug" and aborts non-retryably. So in addition to the missed backfill, the user sees ❌ Non-retryable client error (HTTP None). Aborting. with no traceback in normal logs (only in --dev mode if at all), making this very hard to diagnose without source patching.

Code Example

# venv/lib/python3.11/site-packages/openai/lib/_parsing/_responses.py:61
for output in response.output:
TypeError: 'NoneType' object is not iterable

---

⚠️  API call failed (attempt 1/3): TypeError
      📝 Error: 'NoneType' object is not iterable
Non-retryable client error (HTTP None). Aborting.

---

Traceback (most recent call last):
  File "~/.hermes/hermes-agent/agent/conversation_loop.py", line 1172, in run_conversation
    response = agent._interruptible_streaming_api_call(...)
  File "~/.hermes/hermes-agent/run_agent.py", line 3250, in _interruptible_streaming_api_call
    return interruptible_streaming_api_call(self, api_kwargs, on_first_delta=on_first_delta)
  File "~/.hermes/hermes-agent/agent/chat_completion_helpers.py", line 1400, in interruptible_streaming_api_call
    return agent._interruptible_api_call(api_kwargs)
  File "~/.hermes/hermes-agent/run_agent.py", line 3077, in _interruptible_api_call
    return interruptible_api_call(self, api_kwargs)
  File "~/.hermes/hermes-agent/agent/chat_completion_helpers.py", line 414, in interruptible_api_call
    raise result["error"]
  File "~/.hermes/hermes-agent/agent/chat_completion_helpers.py", line 208, in _call
    result["response"] = agent._run_codex_stream(...)
  File "~/.hermes/hermes-agent/run_agent.py", line 2733, in _run_codex_stream
    return run_codex_stream(self, api_kwargs, client, on_first_delta)
  File "~/.hermes/hermes-agent/agent/codex_runtime.py", line 197, in run_codex_stream
    for event in stream:
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 49, in __iter__
    for item in self._iterator:
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 57, in __stream__
    events_to_fire = self._state.handle_event(sse_event)
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 248, in handle_event
    self.__current_snapshot = snapshot = self.accumulate_event(event)
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 360, in accumulate_event
    self._completed_response = parse_response(...)
  File ".../site-packages/openai/lib/_parsing/_responses.py", line 61, in parse_response
    for output in response.output:
TypeError: 'NoneType' object is not iterable

---

Hermes Agent v0.14.0 (2026.5.16)
git rev: bb4703c76 docs(auth): replace stale 'hermes login' references with 'hermes auth add'
OpenAI SDK: 2.24.0 (and 2.38.0 — both reproduce)

---

response.created
response.in_progress
response.reasoning_summary_part.added
response.reasoning_summary_text.delta
response.reasoning_summary_text.done
response.reasoning_summary_part.done
reasoning                                  (SDK-synthesized "done" event)
response.output_item.added
response.content_part.added
response.output_text.delta
response.output_text.done
response.content_part.done
response.output_item.done                  (assistant message: a short text reply)
output_text                                (SDK-synthesized)
message                                    (SDK-synthesized)
response.output_item.added                 (function_call: a bundled tool)
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done                  (function_call: completed with valid arguments)
function_call                              (SDK-synthesized)

---

def parse_response(*, text_format, input_tools, response):
    output_list: List[ParsedResponseOutputItem[TextFormatT]] = []
    for output in response.output:        # ← TypeError here when response.output is None

---

final_response = stream.get_final_response()        # ← raises TypeError, never returns
# PATCH: ChatGPT Codex backend streams valid output items
# but get_final_response() can return an empty output list.
# Backfill from collected items or synthesize from deltas.
_out = getattr(final_response, "output", None)
if isinstance(_out, list) and not _out:
    if collected_output_items:
        final_response.output = list(collected_output_items)
    ...

---

is_local_validation_error = (
    isinstance(api_error, (ValueError, TypeError))
    and not isinstance(api_error, (UnicodeEncodeError, json.JSONDecodeError))
    ...
)

---

# SDK monkey-patch: chatgpt.com/backend-api/codex closes streams after the
# final `response.output_item.done` without sending the terminal
# `response.completed` event. The SDK's `parse_response()` then crashes on
# `for output in response.output` because `response.output` is None. We
# None-guard at the SDK boundary so the SDK returns an empty ParsedResponse,
# letting our existing backfill (run_codex_stream lines ~265-280) recover
# the collected_output_items into `final_response.output`.
try:
    import openai.lib._parsing._responses as _openai_parse_responses
    _hermes_orig_parse_response = _openai_parse_responses.parse_response

    def _hermes_patched_parse_response(*, text_format, input_tools, response):
        if getattr(response, "output", None) is None:
            response.output = []
        return _hermes_orig_parse_response(
            text_format=text_format, input_tools=input_tools, response=response,
        )

    _openai_parse_responses.parse_response = _hermes_patched_parse_response
except Exception as _e:
    logger.warning(
        "Hermes: failed to apply openai SDK parse_response None-guard (%s); "
        "Codex streams that close without response.completed may crash with TypeError.",
        _e,
    )

---

except (RuntimeError, TypeError) as exc:
    err_text = str(exc)
    missing_completed = "response.completed" in err_text
    prelude_error = (
        "Expected to have received `response.created`" in err_text
        or "Expected to have received \"response.created\"" in err_text
    )
    # New: openai SDK >=2.24 raises TypeError from parse_response when
    # `response.output` is None — same root cause as the older
    # `Expected response.completed` RuntimeError, different shape.
    sdk_output_none = (
        isinstance(exc, TypeError)
        and "NoneType" in err_text
        and "iterable" in err_text
    )
    if (missing_completed or prelude_error or sdk_output_none) and attempt < max_stream_retries:
        ...continue...
    if missing_completed or prelude_error or sdk_output_none:
        return agent._run_codex_create_stream_fallback(api_kwargs, client=active_client)
    raise
RAW_BUFFERClick to expand / collapse

[Bug]: Codex Responses stream ending without response.completed crashes with TypeError: 'NoneType' object is not iterable (TypeError bypasses existing backfill at codex_runtime.py:265-280)

Draft for filing at https://github.com/NousResearch/hermes-agent/issues/new?template=bug_report.yml

Fill in the Debug Report section locally with hermes debug share (or hermes debug share --local if you'd rather inspect what's uploaded first) before submitting.


Bug Description

The chatgpt.com/backend-api/codex Responses backend ends streams immediately after the final response.output_item.done event without sending the terminal response.completed event. The openai SDK's accumulator leaves response.output = None (instead of an empty list) on the snapshot, and parse_response() then crashes when stream.get_final_response() is called:

# venv/lib/python3.11/site-packages/openai/lib/_parsing/_responses.py:61
for output in response.output:
TypeError: 'NoneType' object is not iterable

Hermes already has dedicated backfill logic for this exact case at agent/codex_runtime.py:265-280 (with explanatory comments referencing the chatgpt.com backend) — but it never executes, because get_final_response() raises before control reaches the backfill block. The existing exception handler at codex_runtime.py:302 only catches RuntimeError (the older Expected to have received \response.completed`shape from earlier openai SDK versions), so the newerTypeError` shape jumps past every recovery path and is escalated as a non-retryable client error.

Net effect: every Codex turn that ends on a tool call (or any path where the backend omits response.completed) crashes with the cryptic 'NoneType' object is not iterable and aborts the session, with no actionable error message to the user.

Steps to Reproduce

  1. hermes auth add openai-codex (any ChatGPT-tier OAuth)
  2. Pick any openai-codex model: hermes modelgpt-5.5, gpt-5.4, gpt-5.3-codex (all reproduce — bug is provider-level, not model-level)
  3. Have a non-trivial toolset enabled — observed with 66 registered tools (a couple of MCP servers + the bundled toolset), but the trigger is the terminal-event shape, not tool count
  4. Send any message: hermes chat -q "say the word ok" --provider openai-codex -m gpt-5.5
  5. The model generates valid reasoning + a real assistant message + a real tool call (visible in stream events), then the stream closes
  6. hermes prints:
    ⚠️  API call failed (attempt 1/3): TypeError
       📝 Error: 'NoneType' object is not iterable
    ❌ Non-retryable client error (HTTP None). Aborting.

Reproducible 45+ times in a single session here (see agent.log grep -c NoneType count).

Expected Behavior

When the Codex backend ends a stream without response.completed, the existing backfill code at codex_runtime.py:265-280 (collected_output_itemsfinal_response.output) should kick in and surface the response normally — exactly as it does today for the "SDK returns empty output list" sibling case. The user should receive the model's response (message + tool call) just like the official codex CLI does against the same account/network.

Actual Behavior

stream.get_final_response() raises TypeError: 'NoneType' object is not iterable from inside openai/lib/_parsing/_responses.py:61, which is caught at conversation_loop.py:2825 as a is_local_validation_error = True ("programming bug") and aborted as a non-retryable client error. Full traceback:

Traceback (most recent call last):
  File "~/.hermes/hermes-agent/agent/conversation_loop.py", line 1172, in run_conversation
    response = agent._interruptible_streaming_api_call(...)
  File "~/.hermes/hermes-agent/run_agent.py", line 3250, in _interruptible_streaming_api_call
    return interruptible_streaming_api_call(self, api_kwargs, on_first_delta=on_first_delta)
  File "~/.hermes/hermes-agent/agent/chat_completion_helpers.py", line 1400, in interruptible_streaming_api_call
    return agent._interruptible_api_call(api_kwargs)
  File "~/.hermes/hermes-agent/run_agent.py", line 3077, in _interruptible_api_call
    return interruptible_api_call(self, api_kwargs)
  File "~/.hermes/hermes-agent/agent/chat_completion_helpers.py", line 414, in interruptible_api_call
    raise result["error"]
  File "~/.hermes/hermes-agent/agent/chat_completion_helpers.py", line 208, in _call
    result["response"] = agent._run_codex_stream(...)
  File "~/.hermes/hermes-agent/run_agent.py", line 2733, in _run_codex_stream
    return run_codex_stream(self, api_kwargs, client, on_first_delta)
  File "~/.hermes/hermes-agent/agent/codex_runtime.py", line 197, in run_codex_stream
    for event in stream:
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 49, in __iter__
    for item in self._iterator:
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 57, in __stream__
    events_to_fire = self._state.handle_event(sse_event)
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 248, in handle_event
    self.__current_snapshot = snapshot = self.accumulate_event(event)
  File ".../site-packages/openai/lib/streaming/responses/_responses.py", line 360, in accumulate_event
    self._completed_response = parse_response(...)
  File ".../site-packages/openai/lib/_parsing/_responses.py", line 61, in parse_response
    for output in response.output:
TypeError: 'NoneType' object is not iterable

Confirmed identical traceback on openai==2.24.0 AND openai==2.38.0 — line 61 of _parsing/_responses.py is the same in both, so SDK upgrade alone does not resolve it.

Affected Component

  • Agent Core (conversation loop, codex stream runtime, error classifier)

Messaging Platform

  • N/A (CLI only — reproduced via hermes chat -q; also fires in Telegram gateway turns)

Debug Report

Skipping the public hermes debug share upload — the bug reproduces from a minimal one-shot hermes chat -q "..." --provider openai-codex invocation, and the traceback + 124-event stream trace below already capture all the relevant runtime state. Happy to provide a full hermes debug share paste (or any specific log slice) on request from maintainers.

Operating System

macOS 15.x (Darwin 25.5.0, Apple Silicon)

Python Version

3.11.15

Hermes Version

Hermes Agent v0.14.0 (2026.5.16)
git rev: bb4703c76 docs(auth): replace stale 'hermes login' references with 'hermes auth add'
OpenAI SDK: 2.24.0 (and 2.38.0 — both reproduce)

Additional Logs / Traceback (Stream event trace)

Captured 124 events for a single failing turn by adding a temporary one-liner inside the for event in stream: loop at codex_runtime.py:197 that dumped event.type and key payload fields to disk. Distinct event types received:

response.created
response.in_progress
response.reasoning_summary_part.added
response.reasoning_summary_text.delta
response.reasoning_summary_text.done
response.reasoning_summary_part.done
reasoning                                  (SDK-synthesized "done" event)
response.output_item.added
response.content_part.added
response.output_text.delta
response.output_text.done
response.content_part.done
response.output_item.done                  (assistant message: a short text reply)
output_text                                (SDK-synthesized)
message                                    (SDK-synthesized)
response.output_item.added                 (function_call: a bundled tool)
response.function_call_arguments.delta
response.function_call_arguments.done
response.output_item.done                  (function_call: completed with valid arguments)
function_call                              (SDK-synthesized)

No response.completed, no response.failed, no response.incomplete ever fired. The stream simply closed after the last response.output_item.done.

The model produced a complete, valid response (visible reasoning + a real assistant message + a real skill_view tool call with proper arguments). It's just that the terminal event is missing — exactly the scenario the existing backfill at codex_runtime.py:265-280 is designed to cover, but the SDK's TypeError prevents Hermes from reaching that code.

Root Cause Analysis

Where the iteration happens

openai/lib/_parsing/_responses.py:61 (openai SDK 2.24.0 and 2.38.0):

def parse_response(*, text_format, input_tools, response):
    output_list: List[ParsedResponseOutputItem[TextFormatT]] = []
    for output in response.output:        # ← TypeError here when response.output is None

Why response.output is None

The SDK's ResponsesStreamingState.accumulate_event() (openai/lib/streaming/responses/_responses.py:360) only populates self._completed_response when a response.completed (or terminal failure) event arrives. The chatgpt.com Codex backend ends some streams without such an event, so _completed_response is never set and the snapshot's output attribute remains its zero-state value of None.

The official codex CLI tolerates this (works against the same OAuth account/network/account-ID without crashing), so the workaround belongs on the client side.

Why Hermes' existing backfill doesn't run

agent/codex_runtime.py:261-285:

final_response = stream.get_final_response()        # ← raises TypeError, never returns
# PATCH: ChatGPT Codex backend streams valid output items
# but get_final_response() can return an empty output list.
# Backfill from collected items or synthesize from deltas.
_out = getattr(final_response, "output", None)
if isinstance(_out, list) and not _out:
    if collected_output_items:
        final_response.output = list(collected_output_items)
    ...

The line 261 call raises, so the carefully-engineered backfill on lines 262-284 (which has the explanatory comment about this exact backend!) is never reached.

agent/codex_runtime.py:302 then catches RuntimeError for the older "Expected to have received \response.completed`"SDK shape and falls back via_run_codex_create_stream_fallback. But the openai SDK no longer raises RuntimeErrorfor this case — it raisesTypeErrorfromparse_response`. The except clause misses it.

The is_local_validation_error classifier amplifies

At agent/conversation_loop.py:2825-2829:

is_local_validation_error = (
    isinstance(api_error, (ValueError, TypeError))
    and not isinstance(api_error, (UnicodeEncodeError, json.JSONDecodeError))
    ...
)

This treats any leaked TypeError as a "local programming bug" and aborts non-retryably. So in addition to the missed backfill, the user sees ❌ Non-retryable client error (HTTP None). Aborting. with no traceback in normal logs (only in --dev mode if at all), making this very hard to diagnose without source patching.

Proposed Fix

Two options. Option A is the minimal local fix (recommended); Option B is more defensive.

Option A — None-guard at the SDK boundary (local monkey-patch)

Add a small monkey-patch at the top of agent/codex_runtime.py (right after the logger = logging.getLogger(__name__) line) that None-guards openai.lib._parsing._responses.parse_response:

# SDK monkey-patch: chatgpt.com/backend-api/codex closes streams after the
# final `response.output_item.done` without sending the terminal
# `response.completed` event. The SDK's `parse_response()` then crashes on
# `for output in response.output` because `response.output` is None. We
# None-guard at the SDK boundary so the SDK returns an empty ParsedResponse,
# letting our existing backfill (run_codex_stream lines ~265-280) recover
# the collected_output_items into `final_response.output`.
try:
    import openai.lib._parsing._responses as _openai_parse_responses
    _hermes_orig_parse_response = _openai_parse_responses.parse_response

    def _hermes_patched_parse_response(*, text_format, input_tools, response):
        if getattr(response, "output", None) is None:
            response.output = []
        return _hermes_orig_parse_response(
            text_format=text_format, input_tools=input_tools, response=response,
        )

    _openai_parse_responses.parse_response = _hermes_patched_parse_response
except Exception as _e:
    logger.warning(
        "Hermes: failed to apply openai SDK parse_response None-guard (%s); "
        "Codex streams that close without response.completed may crash with TypeError.",
        _e,
    )

After this patch the existing backfill code at codex_runtime.py:265-280 does the rest. Tested locally — hermes chat -q "say the word ok" against openai-codex / gpt-5.5 now returns cleanly ("ok") instead of crashing. Stream events are identical; the only change is that the TypeError no longer escapes the SDK.

Option B — Catch TypeError in run_codex_stream's except clause

In codex_runtime.py:302, extend the except clause to also catch TypeError:

except (RuntimeError, TypeError) as exc:
    err_text = str(exc)
    missing_completed = "response.completed" in err_text
    prelude_error = (
        "Expected to have received `response.created`" in err_text
        or "Expected to have received \"response.created\"" in err_text
    )
    # New: openai SDK >=2.24 raises TypeError from parse_response when
    # `response.output` is None — same root cause as the older
    # `Expected response.completed` RuntimeError, different shape.
    sdk_output_none = (
        isinstance(exc, TypeError)
        and "NoneType" in err_text
        and "iterable" in err_text
    )
    if (missing_completed or prelude_error or sdk_output_none) and attempt < max_stream_retries:
        ...continue...
    if missing_completed or prelude_error or sdk_output_none:
        return agent._run_codex_create_stream_fallback(api_kwargs, client=active_client)
    raise

This routes the failure into the existing _run_codex_create_stream_fallback path. The downside: it discards the items already collected during the for-loop and issues a fresh non-streaming API call, doubling the request cost for every affected turn.

Recommended: Option A. It preserves the already-collected items and reuses the existing tested backfill code path (no fresh API call, no double-billing). Option B is a fallback if maintainers prefer not to monkey-patch the SDK.

A third path — fix in the OpenAI SDK itself (None-guard parse_response) — would be the truly correct fix but requires an upstream PR there and a coordinated SDK version bump in Hermes.

PR Readiness

  • I'd like to fix this myself and submit a PR (Option A patch ready, ~17 lines, tested locally; happy to also implement Option B if preferred)

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING