Skip to content

fix(codex): handle None response.output in OpenAI SDK stream parser - #33173

Closed
blazing-mj wants to merge 11 commits into
NousResearch:mainfrom
blazing-mj:fix-codex-none-output
Closed

blazing-mj wants to merge 11 commits into
NousResearch:mainfrom
blazing-mj:fix-codex-none-output

Conversation

@blazing-mj

Copy link
Copy Markdown

Problem

ChatGPT's Codex backend (chatgpt.com/backend-api/codex) returns response.output = null in completion events, causing the OpenAI SDK to crash with:

TypeError: 'NoneType' object is not iterable

This kills every turn before Hermes' empty-output backfill can run.

Solution

  • Defensive monkey-patch wrapper around OpenAI SDK's parse_response() that coerces None[]
  • Patches both _sdk_parse and _sdk_stream modules (streaming imports by-name at import time)
  • Idempotent guard prevents double-patching
  • Graceful fallback if SDK shape changes

Also adds full traceback logging for unexpected errors (TypeError/AttributeError/etc) in API call paths.

Testing

✅ Verified on ChatGPT Codex backend with gpt-5.5 model
✅ Zero errors after fix deployed
✅ Previously: TypeError on every API call

Impact

Fixes openai-codex provider for all Hermes users running against ChatGPT backend.

Alfred Crane and others added 11 commits May 22, 2026 15:34
Pin default gateway argv with --profile, warn on sticky active_profile redirects, log gateway boot profile drift, add profile gateway status visibility, and harden related fallback/config paths.

Verified: targeted pytest slices passed; py_compile passed; static scan clean; Claude Code Max review PASS; dashboard HTTP 200.
Background — the existing tool-result persistence layer (Layer 2:
maybe_persist_tool_result) only kicks in past 100,000 chars (~25K
tokens). A mid-sized search_files dump at 56K chars is ~14K tokens —
under the persistence threshold yet still large enough to push the
session past the compression trigger after a few calls.

Add cap_tool_result_tokens as a defensive Layer 2a that runs AFTER
persistence but BEFORE the result is appended to the message list:
when a single tool result exceeds ~8K estimated tokens, slice the
head to ~6K tokens and append a "[truncated NNNN tokens]" marker so
the model knows information was dropped. Multimodal (list) content is
passed through untouched — image budgeting is handled elsewhere.

Wired into both the concurrent and sequential paths in tool_executor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forensics — a session showed:

    Preflight compression: ~67,042 tokens >= 64,000 threshold
    (model gpt-5.5, ctx 272,000)

64,000 is 0.20 * 320,000 (the old default), not 0.30 * 272,000 = 81,600
that the user's current config.yaml dictates. The threshold resolution
path is correct (agent_init reads compression.threshold from config) so
the live runtime was simply stale relative to the on-disk config and
needed a gateway restart.

Add an INFO log line at AIAgent boot (and a small helper in
conversation_compression to format it) that surfaces every knob —
threshold %, target %, protect_first/_last, context_len, and the
absolute trigger_at token count. Future stale-config / stale-runtime
mismatches show up immediately by diffing this line against config.yaml.

    Compression configured: threshold=30% target=10% protect_first=3
    protect_last=8 context_len=272000 trigger_at=81600 tokens

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Forensics — observed in agent.log:

    context compression done: messages=30->30 tokens=~66721->57363
    context compression done: messages=62->62 tokens=~65646->50434
    ...

Same message count in and out: when protect_first_n + protect_last_n
covers (or nearly covers) the whole transcript, the compressor has
nothing to drop and just re-summarises inline. Each pass burns Haiku
tokens, only nibbles a few % off the window, and never crosses back
below the trigger threshold — so the next tool result re-triggers
preflight on the next iteration and the cycle repeats.

Guard:
  1. Add compression_made_progress() — returns False when the message
     count is unchanged AND token reduction is below 15%.
  2. After each preflight pass, run the predicate; if it says no
     progress, log "compression-no-progress, aborting compress loop
     for this turn" and latch _compression_no_progress_turn on the
     agent.
  3. Reset the latch at the top of each turn (alongside the other
     per-turn flag resets).
  4. At the post-tool-call compression site, gate the call on
     should_skip_compression_for_turn(agent) so the same turn does
     not retry compression on every subsequent tool batch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ChatGPT's Codex backend (chatgpt.com/backend-api/codex) emits
response.completed events with response.output set to null instead of
an empty list when output items were produced via output_item.done but
the final accumulation came back empty.

The OpenAI SDK's parse_response() blindly iterates response.output
(openai/lib/_parsing/_responses.py:61), raising:
  TypeError: 'NoneType' object is not iterable

This kills the entire turn before Hermes' existing empty-output
backfill (codex_runtime.py:~247) can synthesize output from streamed
deltas.

Solution:
- Install a defensive monkey-patch wrapper around parse_response() that
  coerces response.output = None → [] before iteration
- Patch both _sdk_parse and _sdk_stream modules (streaming module
  imports parse_response by name at import time)
- Idempotent guard prevents double-patching
- Graceful fallback if SDK internals change

Also adds full traceback logging for TypeError/AttributeError/ValueError
in API call paths (conversation_loop.py) to aid future debugging.

Fixes: openai-codex provider failing with 'NoneType' object is not iterable
Tested: Verified on ChatGPT Codex backend (gpt-5.5 model)
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/openai OpenAI / Codex Responses API codex duplicate This issue or pull request already exists labels May 27, 2026
@alt-glitch

Copy link
Copy Markdown

The core Codex null-output fix in this PR duplicates merged #32963 which already landed the same _install_openai_parse_response_none_guard() approach. This PR also bundles 28+ unrelated files (docs/plans, skills, gateway, CLI, Gemini plugin, scripts) alongside the 5-file Codex fix — 9590 additions across 33 files. The unrelated changes should be separate PRs.

@teknium1

Copy link
Copy Markdown
Collaborator

Closing as obsolete. PR #33042 (merged commit cb38ce28c on main today) removes the OpenAI SDK's client.responses.stream(...) helper from both Codex call sites entirely — we now use client.responses.create(stream=True) raw event iteration that assembles the final response from response.output_item.done events as they arrive. The terminal event's output field is never read for content reconstruction, so the SDK's TypeError: 'NoneType' object is not iterable parser crash on output=null is structurally impossible — no code path can produce it. Same defensive strategy OpenClaw uses for the same backend.

If you're still seeing this on hermes update, your install may be a pip/PyPI install which is still pinned to v0.14.0 (May 16, before today's fixes). A new release is being cut shortly. Git clones of main already have the fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint duplicate This issue or pull request already exists P3 Low — cosmetic, nice to have provider/openai OpenAI / Codex Responses API type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants