mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-18 09:51:59 +00:00
feat(aux): translate extra_body.reasoning into Codex Responses API (#17004)
Auxiliary callers that configure reasoning via
auxiliary.<task>.extra_body.reasoning were having that config silently
dropped by the Codex Responses adapter — it only forwarded
messages/model/tools through to responses.stream(), never translating
chat.completions-shaped reasoning hints into the Responses API's
top-level reasoning + include fields.
Mirror the main-agent translation from agent/transports/codex.py:
- extra_body.reasoning.effort → resp_kwargs.reasoning.{effort, summary:"auto"}
- 'minimal' → 'low' clamp (Codex backend rejects 'minimal')
- Always include ['reasoning.encrypted_content'] when reasoning is enabled
- {'enabled': False} → omit reasoning and include entirely
- Non-dict reasoning values are ignored defensively
Reported by @OP (Apr 26 feedback bundle).
## Changes
- agent/auxiliary_client.py: _CodexCompletionsAdapter.create() now reads
and translates extra_body.reasoning before calling responses.stream()
- tests/agent/test_auxiliary_client.py: 9 new tests covering all effort
levels, the minimal→low clamp, the disabled path, the no-op paths,
and defensive handling of wrong-shape inputs
Co-authored-by: teknium1 <teknium@users.noreply.github.com>
This commit is contained in:
parent
72dea9f4f7
commit
391f1ca1f4
2 changed files with 153 additions and 0 deletions
|
|
@ -410,6 +410,33 @@ class _CodexCompletionsAdapter:
|
|||
# Note: the Codex endpoint (chatgpt.com/backend-api/codex) does NOT
|
||||
# support max_output_tokens or temperature — omit to avoid 400 errors.
|
||||
|
||||
# Translate extra_body.reasoning (chat.completions shape) into the
|
||||
# Responses API's top-level reasoning + include fields. Mirrors
|
||||
# agent/transports/codex.py::build_kwargs() so auxiliary callers
|
||||
# that configure reasoning via auxiliary.<task>.extra_body get the
|
||||
# same behavior as the main agent's Codex transport.
|
||||
extra_body = kwargs.get("extra_body") or {}
|
||||
if isinstance(extra_body, dict):
|
||||
reasoning_cfg = extra_body.get("reasoning")
|
||||
if isinstance(reasoning_cfg, dict):
|
||||
if reasoning_cfg.get("enabled") is False:
|
||||
# Reasoning explicitly disabled — do not set reasoning
|
||||
# or include. The Codex backend still thinks by
|
||||
# default, but we honor the caller's intent where the
|
||||
# API allows it.
|
||||
pass
|
||||
else:
|
||||
effort = reasoning_cfg.get("effort", "medium")
|
||||
# Codex backend rejects "minimal"; clamp to "low" to
|
||||
# match the main-agent Codex transport behavior.
|
||||
if effort == "minimal":
|
||||
effort = "low"
|
||||
resp_kwargs["reasoning"] = {
|
||||
"effort": effort,
|
||||
"summary": "auto",
|
||||
}
|
||||
resp_kwargs["include"] = ["reasoning.encrypted_content"]
|
||||
|
||||
# Tools support for auxiliary callers (e.g. skills_hub) that pass function schemas
|
||||
tools = kwargs.get("tools")
|
||||
if tools:
|
||||
|
|
|
|||
|
|
@ -1509,3 +1509,129 @@ class TestAuxiliaryAuthRefreshRetry:
|
|||
mock_refresh.assert_called_once_with("anthropic")
|
||||
assert stale_client.chat.completions.create.await_count == 1
|
||||
assert fresh_client.chat.completions.create.await_count == 1
|
||||
|
||||
|
||||
class TestCodexAdapterReasoningTranslation:
|
||||
"""Verify _CodexCompletionsAdapter translates extra_body.reasoning
|
||||
into the Responses API's top-level reasoning + include fields, matching
|
||||
agent/transports/codex.py::build_kwargs() behavior.
|
||||
|
||||
Regression for user feedback (Apr 26): auxiliary callers that configure
|
||||
reasoning via auxiliary.<task>.extra_body.reasoning had that config
|
||||
silently dropped because the adapter only forwarded messages/model/tools.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _build_adapter():
|
||||
"""Build a _CodexCompletionsAdapter with a mocked responses.stream()."""
|
||||
from agent.auxiliary_client import _CodexCompletionsAdapter
|
||||
from types import SimpleNamespace
|
||||
|
||||
# Mock the stream context manager: yields no events, get_final_response
|
||||
# returns a minimal empty-output response.
|
||||
fake_final = SimpleNamespace(
|
||||
output=[SimpleNamespace(
|
||||
type="message",
|
||||
content=[SimpleNamespace(type="output_text", text="hi")],
|
||||
)],
|
||||
usage=SimpleNamespace(input_tokens=1, output_tokens=1, total_tokens=2),
|
||||
)
|
||||
|
||||
class _FakeStream:
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *a): return False
|
||||
def __iter__(self): return iter([])
|
||||
def get_final_response(self): return fake_final
|
||||
|
||||
captured_kwargs = {}
|
||||
|
||||
def _stream(**kwargs):
|
||||
captured_kwargs.update(kwargs)
|
||||
return _FakeStream()
|
||||
|
||||
real_client = MagicMock()
|
||||
real_client.responses.stream = _stream
|
||||
adapter = _CodexCompletionsAdapter(real_client, "gpt-5.3-codex")
|
||||
return adapter, captured_kwargs
|
||||
|
||||
def test_reasoning_effort_medium_translated_to_top_level(self):
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": {"effort": "medium"}},
|
||||
)
|
||||
assert captured.get("reasoning") == {"effort": "medium", "summary": "auto"}
|
||||
assert captured.get("include") == ["reasoning.encrypted_content"]
|
||||
|
||||
def test_reasoning_effort_minimal_clamped_to_low(self):
|
||||
"""Codex backend rejects 'minimal'; adapter clamps to 'low' per main transport."""
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": {"effort": "minimal"}},
|
||||
)
|
||||
assert captured.get("reasoning") == {"effort": "low", "summary": "auto"}
|
||||
assert captured.get("include") == ["reasoning.encrypted_content"]
|
||||
|
||||
def test_reasoning_effort_low_passed_through(self):
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": {"effort": "low"}},
|
||||
)
|
||||
assert captured.get("reasoning") == {"effort": "low", "summary": "auto"}
|
||||
|
||||
def test_reasoning_effort_high_passed_through(self):
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": {"effort": "high"}},
|
||||
)
|
||||
assert captured.get("reasoning") == {"effort": "high", "summary": "auto"}
|
||||
|
||||
def test_reasoning_disabled_omits_reasoning_and_include(self):
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": {"enabled": False}},
|
||||
)
|
||||
assert "reasoning" not in captured
|
||||
assert "include" not in captured
|
||||
|
||||
def test_reasoning_default_effort_when_only_enabled_flag(self):
|
||||
"""extra_body={"reasoning": {}} (truthy enabled by omission) → default 'medium'."""
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": {}},
|
||||
)
|
||||
assert captured.get("reasoning") == {"effort": "medium", "summary": "auto"}
|
||||
assert captured.get("include") == ["reasoning.encrypted_content"]
|
||||
|
||||
def test_no_extra_body_means_no_reasoning_keys(self):
|
||||
"""Baseline: without extra_body, no reasoning/include is sent (preserves
|
||||
current behavior for callers that don't opt in)."""
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(messages=[{"role": "user", "content": "hi"}])
|
||||
assert "reasoning" not in captured
|
||||
assert "include" not in captured
|
||||
|
||||
def test_extra_body_without_reasoning_key_is_noop(self):
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"metadata": {"source": "test"}},
|
||||
)
|
||||
assert "reasoning" not in captured
|
||||
assert "include" not in captured
|
||||
|
||||
def test_non_dict_reasoning_value_is_ignored_gracefully(self):
|
||||
"""Defensive: if a caller accidentally passes a string/None, we
|
||||
silently skip instead of crashing inside the adapter."""
|
||||
adapter, captured = self._build_adapter()
|
||||
adapter.create(
|
||||
messages=[{"role": "user", "content": "hi"}],
|
||||
extra_body={"reasoning": "medium"}, # wrong shape — must not crash
|
||||
)
|
||||
assert "reasoning" not in captured
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue