Skip to content

fix(codex): avoid tools=None TypeError on openai-codex when no tools are registered (#32892)#32911

Open
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:fix/32892-codex-nonetype-iterable
Open

fix(codex): avoid tools=None TypeError on openai-codex when no tools are registered (#32892)#32911
xxxigm wants to merge 3 commits into
NousResearch:mainfrom
xxxigm:fix/32892-codex-nonetype-iterable

Conversation

@xxxigm

@xxxigm xxxigm commented May 27, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes #32892 — every Hermes turn against openai-codex / gpt-5.5
(chatgpt.com/backend-api/codex) crashed before sending the request when
the agent was running without external tools registered, surfacing as:

⚠️  API call failed (attempt 1/3): TypeError
   🔌 Provider: openai-codex  Model: gpt-5.5
   🌐 Endpoint: https://chatgpt.com/backend-api/codex
   📝 Error: 'NoneType' object is not iterable
⚠️ Non-retryable error (HTTP None) — trying fallback...
❌ Non-retryable client error (HTTP None). Aborting.

Root cause

openai==2.24.0's responses.stream() (and responses.parse()) call
_make_tools(tools) eagerly:

def _make_tools(tools):
    if not is_given(tools):
        return omit
    converted_tools = []
    for tool in tools:           # ← iterates without a None guard
        ...

is_given(None) returns True, so passing tools=None skips the
omit early-return and crashes with
TypeError: 'NoneType' object is not iterable before any HTTP request
is issued. The agent's outer loop catches the TypeError, sees no HTTP
status, classifies it as non-retryable and aborts.

Hermes' Codex transport was unconditionally placing "tools": response_tools into the kwargs dict, so response_tools=None
(the "no tools registered" case) shipped tools=None straight into the
SDK.

Fix (two defences, three small commits)

  1. agent/transports/codex.py::build_kwargs — only attach tools
    (and the meaningless tool_choice / parallel_tool_calls partners)
    when the converted list is non-empty. Matches how the
    chat_completions transport already gates the same keys.
  2. agent/codex_runtime.py::_strip_sdk_none_iterables — defensive
    shim called from run_codex_stream and
    run_codex_create_stream_fallback so any future code path that
    still ships tools=None (a custom transport, a plugin re-injecting
    the key, a future preflight regression) is scrubbed at the SDK
    boundary. Mutates in place so debug dumps / retries see exactly
    what was sent.
  3. tests/run_agent/test_codex_no_tools_nonetype.py — 12 new
    tests covering both defences plus an end-to-end run_codex_stream
    reproducer.

Related Issue

Fixes #32892

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/transports/codex.py — drop the unconditional
    "tools": response_tools slot; only add tools /
    tool_choice / parallel_tool_calls when there are real tools.
  • agent/codex_runtime.py — add _strip_sdk_none_iterables, call
    from run_codex_stream and run_codex_create_stream_fallback,
    export from __all__.
  • tests/run_agent/test_codex_no_tools_nonetype.py12 new tests
    pinning build_kwargs, the strip helper, end-to-end through
    run_codex_stream, and an upstream SDK assertion so we notice if
    _make_tools(None) ever gets fixed.

How to Test

# New regression suite — 12 tests, no network
python -m pytest tests/run_agent/test_codex_no_tools_nonetype.py -v

# Wider Codex surface — full existing suite must still pass
python -m pytest tests/run_agent/test_run_agent_codex_responses.py
# expected: 65 passed

Direct reproduction of the upstream bug (and confirmation of the fix):

# BEFORE the fix:
>>> from openai import OpenAI
>>> c = OpenAI(api_key='x', base_url='https://chatgpt.com/backend-api/codex')
>>> c.responses.stream(model='gpt-5.5', input=[{'role':'user','content':'hi'}],
...                    instructions='ok', tools=None, store=False).__enter__()
TypeError: 'NoneType' object is not iterable

# AFTER the fix — build_kwargs never emits tools=None,
# and the strip helper guards the SDK call:
>>> from agent.transports.codex import ResponsesApiTransport
>>> kw = ResponsesApiTransport().build_kwargs(
...     model='gpt-5.5',
...     messages=[{'role':'system','content':'ok'},{'role':'user','content':'hi'}],
...     tools=None, is_codex_backend=True)
>>> 'tools' in kw
False
>>> c.responses.stream(**kw)   # reaches network normally

Checklist

  • Conventional Commits (fix(codex):, fix(codex):, test(codex):)
  • 3 focused commits, single author (xxxigm)
  • 12 new tests pass; existing 65 codex_responses tests pass; the
    remaining failures in the broader run are pre-existing on
    upstream/main (missing optional anthropic SDK,
    test_bundled_plugins_discovered plugin-discovery flake) and
    unrelated to this PR.
  • Tested on macOS 15.6 (darwin 24.6.0), Python 3.12, openai==2.24.0
  • No new config keys, no breaking API change.
  • Backwards-compatible: when tools ARE registered the outgoing
    kwargs are byte-for-byte identical to before; only the toolless
    path changes shape.

xxxigm added 3 commits May 27, 2026 08:09
…arch#32892)

``responses.stream()`` / ``responses.parse()`` in openai==2.24.0
unconditionally invoke ``_make_tools(tools)`` which iterates ``tools``
without a None guard, so passing ``tools=None`` raises
``TypeError: 'NoneType' object is not iterable`` before any HTTP
request is sent. The agent's outer loop catches the TypeError, sees
no HTTP status, classifies it as non-retryable and aborts —
breaking every ``openai-codex`` / ``gpt-5.5`` turn for users who run
Hermes without external tools registered.

Only attach ``tools`` (and the meaningless ``tool_choice`` /
``parallel_tool_calls`` partners) to the kwargs dict when the
converted list is non-empty. Preflight already strips ``tools=None``
on its own pass; keeping the build_kwargs output clean removes the
need for that downstream rescue and matches how the chat_completions
transport already gates the same fields.
Add ``_strip_sdk_none_iterables`` and call it from
``run_codex_stream`` and ``run_codex_create_stream_fallback`` so any
code path that still ships ``tools=None`` (a custom transport, a
plugin re-injecting the key, a future regression in preflight) is
caught before reaching openai==2.24.0's ``_make_tools(None)``.

When ``tools`` is None we also drop ``tool_choice`` and
``parallel_tool_calls`` — both are meaningless without tools and
already 400 on a few Codex-style relays — so the kwargs stay
internally consistent. The helper mutates in place so the caller's
dict reflects what was actually sent to the SDK (debug dumps and
retries pick up the scrubbed shape automatically).
…ch#32892)

Pin both defences against the openai SDK ``_make_tools(None)`` crash:

* ``build_kwargs`` must never emit ``tools=None`` (or the orphaned
  ``tool_choice`` / ``parallel_tool_calls`` partners), and it must
  still surface the key when tools ARE registered.
* ``_strip_sdk_none_iterables`` must scrub ``tools=None`` in place
  while leaving populated tool lists untouched and tolerating
  non-dict inputs.
* End-to-end through ``run_codex_stream`` with a fake stream that
  records the kwargs the SDK saw — the actual NousResearch#32892 reproducer.

A small ``_make_tools(None)`` assertion is included so we notice if
the openai SDK ever fixes the upstream behaviour and the agent
defences become belt-only.
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/agent Core agent loop, run_agent.py, prompt builder provider/openai OpenAI / Codex Responses API codex labels May 27, 2026
teknium1 pushed a commit that referenced this pull request May 27, 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>
mathias3 pushed a commit to mathias3/hermes-agent that referenced this pull request May 28, 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>
Bryce-huang pushed a commit to wbkunlun/hermes-agent that referenced this pull request May 29, 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>

#AI commit#
mosaiq-systems pushed a commit to mosaiq-systems/hermes-agent that referenced this pull request May 29, 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>
KKT-OPT pushed a commit to KKT-OPT/hermes-agent that referenced this pull request May 31, 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>
gweeteve pushed a commit to gweeteve/hermes-agent that referenced this pull request Jun 2, 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

codex comp/agent Core agent loop, run_agent.py, prompt builder 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.

[Bug]: Error: 'NoneType' object is not iterable

2 participants