fix(aux): stop leaking OpenAI response_format onto the Anthropic Messages API - #85626
mmcclean-aws wants to merge 2 commits into
Conversation
…ages API
Session auto-titling failed on every call when title_generation resolved to
an Anthropic-wire provider (Bedrock, or any Anthropic-compatible gateway):
WARNING agent.title_generator: Title generation failed: Error code: 400 -
{'message': 'response_format: Extra inputs are not permitted'}
title_generator sends extra_body={"response_format": _TITLE_RESPONSE_FORMAT}
unconditionally, and _AnthropicCompletionsAdapter.create() forwarded caller
extra_body verbatim into the Messages API body, excluding only "reasoning"
and _-prefixed keys. The Messages API has no response_format field, and
Bedrock rejects unknown body keys outright, so the field failed the whole
request instead of being ignored. Sessions kept their truncated derived
title forever (title_source stayed 'derived').
Strip response_format alongside reasoning via a named frozenset. Degrading is
safe: the title prompt already demands a bare JSON object and
_extract_title_text() falls back to a loose JSON scan, so an unconstrained
reply still titles.
This is a different root cause from the json_schema-rejection reports
(NousResearch#82816, NousResearch#83390, NousResearch#84976) and is not fixed by retrying with a looser
response_format — json_object 400s identically on Bedrock. It also affects
plugin_llm's json_mode/json_schema path, which routes through the same
adapter.
Fixes NousResearch#85624
fix(aux): stop leaking OpenAI No blocking issues found. A few minor observations:
|
Review follow-up on NousResearch#85626. 1. Silent degradation is now observable: emit a debug log naming the dropped keys and model whenever a caller's extra_body contains a key in _ANTHROPIC_UNSUPPORTED_EXTRA_BODY_KEYS. Debug rather than warning because the two known callers (title_generation, plugin_llm json_mode/json_schema) tolerate the degradation by design and a warning would fire on every aux call; a future caller that cannot tolerate it can now see the drop instead of inferring it from a malformed reply. Guarded by isEnabledFor so the set intersection is skipped on the hot path. 2. Close the second-path question: response_format cannot arrive as a top-level kwarg to _AnthropicCompletionsAdapter.create(). The adapter reads a fixed allow-list of OpenAI kwargs and rebuilds the Messages body from scratch, so unrecognized top-level kwargs are dropped rather than forwarded. Add test_anthropic_aux_response_format_top_level_kwarg_not_forwarded to pin that (asserts the key reaches neither build_anthropic_kwargs nor the SDK, top-level or via extra_body) so a future refactor to **kwargs-splat forwarding can't silently reopen the leak. Also adds test_anthropic_aux_logs_dropped_response_format. Verified on live Bedrock (us.anthropic.claude-opus-5, us-west-2): the debug line fires and the call returns 200 with a parseable title.
|
Thanks — both actionable points addressed in 75001c5. 1. Silent degradation (fixed). Agreed: dropping Debug rather than warning, deliberately: both current callers ( 2. Scope of the strip — checked, and the loop is closed. You're right that this shouldn't rest on reading the current code, so I pinned it: Verification The pre-existing Point 3 noted, thanks — keeping the exclusion list in one auditable place was the intent, and the new log reads from the same frozenset, so a future added key is announced without touching the logging code. On the deeper version of point 1: translating |
…adapter The adapter builds the Messages body from a fixed allow-list of kwargs. A caller that passes response_format as a top-level kwarg (the OpenAI SDK call shape) got it dropped on the floor. The request succeeded, but the schema contract silently became prompt compliance. No in-tree caller uses this shape today. The pin-test makes sure that a future refactor cannot open this leak again. The top-level kwarg gets the same output_config.format translation as the extra_body shape. When a caller sends both shapes, the extra_body value wins because every in-tree caller uses that shape. Pin-test pattern from PR #85626 review follow-up. Co-authored-by: Matt McClean <mmcclean@amazon.com>
|
Closing as implemented on main: #89589 (merged as 3c67501) fixes the same failure at the same adapter boundary, and this PR materially shaped it. What #89589 absorbed from here:
For gateways that predate output_config (the docs call out bedrock-mantle), #89589 adds a reactive retry rung that drops the field once and degrades gracefully — so the strip behavior you built survives as the fallback tier rather than the primary path. Thank you for the rigorous diagnosis on #85624; it is the reason this cluster resolved with schema enforcement preserved instead of dropped. |
…adapter The adapter builds the Messages body from a fixed allow-list of kwargs. A caller that passes response_format as a top-level kwarg (the OpenAI SDK call shape) got it dropped on the floor. The request succeeded, but the schema contract silently became prompt compliance. No in-tree caller uses this shape today. The pin-test makes sure that a future refactor cannot open this leak again. The top-level kwarg gets the same output_config.format translation as the extra_body shape. When a caller sends both shapes, the extra_body value wins because every in-tree caller uses that shape. Pin-test pattern from PR NousResearch#85626 review follow-up. Co-authored-by: Matt McClean <mmcclean@amazon.com>
…adapter The adapter builds the Messages body from a fixed allow-list of kwargs. A caller that passes response_format as a top-level kwarg (the OpenAI SDK call shape) got it dropped on the floor. The request succeeded, but the schema contract silently became prompt compliance. No in-tree caller uses this shape today. The pin-test makes sure that a future refactor cannot open this leak again. The top-level kwarg gets the same output_config.format translation as the extra_body shape. When a caller sends both shapes, the extra_body value wins because every in-tree caller uses that shape. Pin-test pattern from PR NousResearch#85626 review follow-up. Co-authored-by: Matt McClean <mmcclean@amazon.com>
…adapter The adapter builds the Messages body from a fixed allow-list of kwargs. A caller that passes response_format as a top-level kwarg (the OpenAI SDK call shape) got it dropped on the floor. The request succeeded, but the schema contract silently became prompt compliance. No in-tree caller uses this shape today. The pin-test makes sure that a future refactor cannot open this leak again. The top-level kwarg gets the same output_config.format translation as the extra_body shape. When a caller sends both shapes, the extra_body value wins because every in-tree caller uses that shape. Pin-test pattern from PR NousResearch#85626 review follow-up. Co-authored-by: Matt McClean <mmcclean@amazon.com>
Problem
Session auto-titling fails on every call when the
title_generationauxiliary task resolves to an Anthropic-wire provider (Bedrock, or any Anthropic-compatible gateway):Sessions keep their truncated derived title forever (
title_sourcestaysderived). This is the default configuration for anyone running Hermes on Bedrock, sinceauxiliary.title_generation.provider: autoresolves to the main provider.Fixes #85624.
Root cause
Two correct-in-isolation behaviours combine into a guaranteed failure:
agent/title_generator.py:404unconditionally sends the OpenAI structured-output field:_AnthropicCompletionsAdapter.create()forwarded callerextra_bodyverbatim into the Messages API body, excluding onlyreasoningand_-prefixed keys.The Anthropic Messages API has no
response_formatfield — structured output is expressed through tools/prefill — and Bedrock's endpoint rejects unknown body keys rather than ignoring them. So the field didn't degrade; it failed the whole request.The adapter already excluded
reasoningfor precisely this reason ("forwarding the raw field alongside would double-specify reasoning and 400 on strict gateways").response_formatis the same class of OpenAI-shaped key and was simply missed.Fix
Strip
response_formatalongsidereasoning, via a named frozenset so the intent is documented in one place and future OpenAI-only keys have an obvious home:Degrading is safe rather than lossy: the title prompt already demands a bare JSON object and
_extract_title_text()falls back to a loose JSON scan, so an unconstrained reply still titles correctly.Why not fix it in
title_generatorinsteadThe two open PRs for the neighbouring
json_schema-rejection reports (#82751, #83186) retry with a looser response_format. That approach cannot fix this case — on Bedrock the looser rung 400s identically, because the problem is the field's existence, not its value:(Raw
AnthropicBedrockagainstus.anthropic.claude-opus-5,us-west-2, one axis varied.)Fixing it at the adapter also covers a second caller:
agent/plugin_llm.py:868,1011(_json_response_format()forjson_mode/json_schema) routes through the samecall_llmpath, so plugin structured-output calls were broken on Anthropic-wire providers too.Verification
Real path, live Bedrock, on this branch (
us.anthropic.claude-opus-5,us-west-2):Before the change the same call raised
400 response_format: Extra inputs are not permitted. A fresh session now recordstitle_source = llminstead ofderived.Tests — two regression cases added to the existing
_run_anthropic_adapterharness intests/agent/test_auxiliary_client.py:test_anthropic_aux_strips_response_format— the key never reaches the SDKtest_anthropic_aux_strips_response_format_keeps_siblings— stripping it does not drop legitimate vendor fields (metadatasurvives)The pre-existing
test_anthropic_aux_extra_body_passthroughstill passes, confirming legitimate vendor passthrough (thinking,metadata) is unaffected.Follow-up (not in this PR)
A richer fix would translate
response_formatinto the Messages API's native structured-output mechanism (a forced single-tool call, or assistant prefill), giving Anthropic-wire providers real schema enforcement instead of best-effort prompt compliance. Happy to do that as a separate PR if wanted — this one restores titling with the minimal change.