mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-29 06:31:32 +00:00
fix(auxiliary): coerce None final.output to empty list in Codex aux adapter
Closes #33368. `_CodexCompletionsAdapter.create()` iterates `final.output` from the Codex Responses stream. The event-driven consumer (introduced in #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 #33368).
This commit is contained in:
parent
9919caff46
commit
486d632cc2
2 changed files with 58 additions and 1 deletions
|
|
@ -2591,6 +2591,63 @@ class TestCodexAuxiliaryAdapterNullOutputRecovery:
|
|||
|
||||
assert response.choices[0].message.content == "aux survived"
|
||||
|
||||
def test_handles_final_output_is_none_after_consumer(self):
|
||||
"""Regression for #33368 — defense against ``final.output`` being ``None``.
|
||||
|
||||
The event-driven consumer always sets ``final.output`` to a list, so this
|
||||
shape can't come from our own path. But a mocked client / compatibility
|
||||
shim that returns a typed Response with ``output=None`` directly (or a
|
||||
future code path that wraps a different consumer) would crash on
|
||||
``for item in getattr(final, "output", [])`` because ``getattr`` returns
|
||||
``None`` (not the default) when the attribute exists but is ``None``.
|
||||
Coerce with ``or []`` to handle this defensively.
|
||||
"""
|
||||
# Stream that returns no items but a terminal with output=None.
|
||||
# The consumer assembles an empty list. We then mock the consumer's
|
||||
# return to simulate a third-party path that returns final.output=None.
|
||||
empty_events = [
|
||||
SimpleNamespace(type="response.completed", response=SimpleNamespace(
|
||||
status="completed", id="r", output=None, usage=None,
|
||||
)),
|
||||
]
|
||||
|
||||
class _Stream:
|
||||
def __iter__(self): return iter(empty_events)
|
||||
def close(self): pass
|
||||
|
||||
# Monkey-patch the consumer to return a final whose .output is None
|
||||
# (mimics third-party shim behavior the defensive guard protects against).
|
||||
from agent import codex_runtime
|
||||
original_consume = codex_runtime._consume_codex_event_stream
|
||||
|
||||
def _consume_returning_none_output(*args, **kwargs):
|
||||
return SimpleNamespace(
|
||||
output=None, # the defensive guard target
|
||||
output_text="",
|
||||
usage=None,
|
||||
status="completed",
|
||||
id="r",
|
||||
model=kwargs.get("model"),
|
||||
incomplete_details=None,
|
||||
error=None,
|
||||
)
|
||||
|
||||
codex_runtime._consume_codex_event_stream = _consume_returning_none_output
|
||||
try:
|
||||
class FakeResponses:
|
||||
def create(self, **kwargs):
|
||||
return _Stream()
|
||||
|
||||
fake_client = SimpleNamespace(responses=FakeResponses())
|
||||
adapter = _CodexCompletionsAdapter(fake_client, "gpt-5.5")
|
||||
|
||||
# Should not raise TypeError: 'NoneType' object is not iterable
|
||||
response = adapter.create(messages=[{"role": "user", "content": "x"}])
|
||||
assert response.choices[0].message.content is None
|
||||
assert response.choices[0].finish_reason == "stop"
|
||||
finally:
|
||||
codex_runtime._consume_codex_event_stream = original_consume
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Issue #23432 — auxiliary timeout poisons cached client; later aux calls fail
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue