Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 44 additions & 2 deletions agent/auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1945,6 +1945,16 @@ def __init__(self, sync_wrapper: "CodexAuxiliaryClient"):
self._real_client = sync_wrapper._real_client


# Caller-supplied ``extra_body`` keys that must never reach the Anthropic
# Messages API. These are OpenAI chat.completions request fields with no
# Messages-API equivalent; strict endpoints (Bedrock) reject unknown body keys
# outright rather than ignoring them, turning a best-effort hint into a hard
# HTTP 400. ``reasoning`` is additionally translated into the native
# ``thinking`` field by build_anthropic_kwargs, so forwarding it would
# double-specify reasoning.
_ANTHROPIC_UNSUPPORTED_EXTRA_BODY_KEYS = frozenset({"reasoning", "response_format"})


class _AnthropicCompletionsAdapter:
"""OpenAI-client-compatible adapter for Anthropic Messages API."""

Expand Down Expand Up @@ -2045,19 +2055,51 @@ def create(self, **kwargs) -> Any:
# form is the documented Anthropic SDK passthrough for non-standard
# request body keys; merge on top of whatever build_anthropic_kwargs
# already produced (e.g. fast-mode ``speed``) so call-time settings
# survive. Two exclusions:
# survive. Three exclusions:
# - ``reasoning``: the OpenAI-shaped config dict is TRANSLATED into
# the native ``thinking`` field above (build_anthropic_kwargs);
# forwarding the raw field alongside would double-specify
# reasoning and 400 on strict gateways.
# - ``response_format``: OpenAI chat.completions-only. The Messages
# API expresses structured output through tools/prefill and has no
# such field, so forwarding it is never useful and is actively
# fatal on strict endpoints — Bedrock rejects unknown body keys
# with ``response_format: Extra inputs are not permitted``, which
# made every ``title_generation`` call fail on Bedrock/Anthropic
# (title_generator sends a json_schema format unconditionally).
# Callers already tolerate an unconstrained reply: the title
# prompt demands a bare JSON object and ``_extract_title_text``
# falls back to a loose JSON scan, so dropping the hint degrades
# gracefully instead of failing the request.
# - ``_``-prefixed keys: private Hermes plumbing (_reasoning_config
# et al.), never wire fields.
caller_extra_body = kwargs.get("extra_body")
if caller_extra_body and isinstance(caller_extra_body, dict):
passthrough = {
k: v for k, v in caller_extra_body.items()
if k != "reasoning" and not str(k).startswith("_")
if k not in _ANTHROPIC_UNSUPPORTED_EXTRA_BODY_KEYS
and not str(k).startswith("_")
}
# Make the degradation observable. Dropping ``response_format``
# silently weakens a caller's contract from schema-enforced to
# best-effort prompt compliance; a future caller that cannot
# tolerate that should be able to see it in the log rather than
# discover it from a malformed reply. Debug level: for the known
# callers (title generation, plugin json_mode) this is expected
# and would otherwise be per-call warning noise.
if logger.isEnabledFor(logging.DEBUG):
dropped = sorted(
_ANTHROPIC_UNSUPPORTED_EXTRA_BODY_KEYS
& set(caller_extra_body.keys())
)
if dropped:
logger.debug(
"Anthropic Messages API: dropped unsupported "
"extra_body keys %s for model %s (no Messages-API "
"equivalent; structured output degrades to prompt "
"compliance)",
dropped, model,
)
if passthrough:
existing = anthropic_kwargs.get("extra_body") or {}
if not isinstance(existing, dict):
Expand Down
88 changes: 88 additions & 0 deletions tests/agent/test_auxiliary_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2386,6 +2386,94 @@ def test_anthropic_aux_extra_body_passthrough(self):
"thinking": {"type": "disabled"}, "metadata": {"user_id": "u1"},
}

def test_anthropic_aux_strips_response_format(self):
"""#85624: response_format must never reach the Messages API.

It is an OpenAI chat.completions-only field. Bedrock rejects unknown
body keys with 'response_format: Extra inputs are not permitted',
which made every title_generation call fail on Bedrock/Anthropic.
"""
api_kwargs = self._run_anthropic_adapter(
call_extra_body={
"response_format": {
"type": "json_schema",
"json_schema": {"name": "session_title", "strict": True},
},
},
)
# Nothing survived, so no extra_body should be attached at all.
assert "response_format" not in (api_kwargs.get("extra_body") or {})
assert "extra_body" not in api_kwargs

def test_anthropic_aux_strips_response_format_keeps_siblings(self):
"""Stripping response_format must not drop legitimate vendor fields."""
api_kwargs = self._run_anthropic_adapter(
call_extra_body={
"response_format": {"type": "json_object"},
"metadata": {"user_id": "u1"},
},
)
assert api_kwargs["extra_body"] == {"metadata": {"user_id": "u1"}}

def test_anthropic_aux_response_format_top_level_kwarg_not_forwarded(self):
"""#85626 review: response_format can't leak as a top-level kwarg either.

The adapter reads a fixed allow-list of OpenAI kwargs (model, messages,
tools, tool_choice, max_tokens, temperature, extra_body) and builds the
Messages body from scratch, so an unrecognized top-level kwarg is
dropped on the floor rather than forwarded. This pins that behaviour so
a future refactor to **kwargs-splat forwarding can't silently reopen
the leak on a second path.
"""
from agent.auxiliary_client import _AnthropicCompletionsAdapter

adapter = _AnthropicCompletionsAdapter(
MagicMock(), "claude-sonnet-4-6", is_oauth=False,
)
bak_result = {
"model": "claude-sonnet-4-6", "messages": [], "max_tokens": 64,
}
with patch("agent.anthropic_adapter.build_anthropic_kwargs",
return_value=dict(bak_result)) as mock_bak, \
patch("agent.anthropic_adapter.create_anthropic_message") as mock_create, \
patch("agent.transports.get_transport") as mock_gt:
mock_gt.return_value.normalize_response.return_value = MagicMock(
content="ok", tool_calls=None, reasoning=None,
finish_reason="stop", usage=None, provider_data=None,
)
adapter.create(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": "hi"}],
max_tokens=64,
# top-level, NOT nested under extra_body
response_format={
"type": "json_schema",
"json_schema": {"name": "session_title", "strict": True},
},
)

# Never reaches the body builder...
assert "response_format" not in mock_bak.call_args.kwargs
api_kwargs = mock_create.call_args.args[1]
# ...nor the SDK, top-level or smuggled into extra_body.
assert "response_format" not in api_kwargs
assert "response_format" not in (api_kwargs.get("extra_body") or {})

def test_anthropic_aux_logs_dropped_response_format(self, caplog):
"""#85626 review: the degradation is observable, not silent."""
with caplog.at_level(logging.DEBUG, logger="agent.auxiliary_client"):
self._run_anthropic_adapter(
call_extra_body={
"response_format": {"type": "json_object"},
"metadata": {"user_id": "u1"},
},
)
assert any(
"dropped unsupported extra_body keys" in r.message
and "response_format" in r.getMessage()
for r in caplog.records
), caplog.text




Expand Down