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:
Teknium 2026-04-28 05:47:42 -07:00 committed by GitHub
parent 72dea9f4f7
commit 391f1ca1f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 153 additions and 0 deletions

View file

@ -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:

View file

@ -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