Skip to content

fix(aux): stop leaking OpenAI response_format onto the Anthropic Messages API - #85626

Closed
mmcclean-aws wants to merge 2 commits into
NousResearch:mainfrom
mmcclean-aws:fix/anthropic-strip-response-format
Closed

mmcclean-aws wants to merge 2 commits into
NousResearch:mainfrom
mmcclean-aws:fix/anthropic-strip-response-format

Conversation

@mmcclean-aws

Copy link
Copy Markdown

Problem

Session auto-titling fails on every call when the title_generation auxiliary task resolves 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'}

Sessions keep their truncated derived title forever (title_source stays derived). This is the default configuration for anyone running Hermes on Bedrock, since auxiliary.title_generation.provider: auto resolves to the main provider.

Fixes #85624.

Root cause

Two correct-in-isolation behaviours combine into a guaranteed failure:

  1. agent/title_generator.py:404 unconditionally sends the OpenAI structured-output field:
    extra_body={"response_format": _TITLE_RESPONSE_FORMAT},   # json_schema, strict
  2. _AnthropicCompletionsAdapter.create() forwarded caller extra_body verbatim into the Messages API body, excluding only reasoning and _-prefixed keys.

The Anthropic Messages API has no response_format field — 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 reasoning for precisely this reason ("forwarding the raw field alongside would double-specify reasoning and 400 on strict gateways"). response_format is the same class of OpenAI-shaped key and was simply missed.

Fix

Strip response_format alongside reasoning, via a named frozenset so the intent is documented in one place and future OpenAI-only keys have an obvious home:

_ANTHROPIC_UNSUPPORTED_EXTRA_BODY_KEYS = frozenset({"reasoning", "response_format"})

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_generator instead

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

400 FAIL  response_format json_schema   (what title_generator sends)
400 FAIL  response_format json_object   (#82751's retry rung)
200 OK    thinking: {"type":"disabled"}  (#83186's other retry key)
200 OK    no extra_body                 (control)

(Raw AnthropicBedrock against us.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() for json_mode/json_schema) routes through the same call_llm path, 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):

strip set  : ['reasoning', 'response_format']
call_llm OK: '{"title":"Debugging AWS Bedrock 400 Error"}'
generate_title(): 'Bedrock 400 error on title generation'

Before the change the same call raised 400 response_format: Extra inputs are not permitted. A fresh session now records title_source = llm instead of derived.

Tests — two regression cases added to the existing _run_anthropic_adapter harness in tests/agent/test_auxiliary_client.py:

  • test_anthropic_aux_strips_response_format — the key never reaches the SDK
  • test_anthropic_aux_strips_response_format_keeps_siblings — stripping it does not drop legitimate vendor fields (metadata survives)
$ pytest tests/agent/test_auxiliary_client.py -k "strips_response_format or extra_body_passthrough"
3 passed, 180 deselected

The pre-existing test_anthropic_aux_extra_body_passthrough still passes, confirming legitimate vendor passthrough (thinking, metadata) is unaffected.

Follow-up (not in this PR)

A richer fix would translate response_format into 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.

…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
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint provider/anthropic Anthropic native Messages API provider/bedrock AWS Bedrock (boto3, IAM) P3 Low — cosmetic, nice to have labels Aug 13, 2026
@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference, author can ignore or act on any point.

fix(aux): stop leaking OpenAI response_format onto the Anthropic Messages API

No blocking issues found. A few minor observations:

  1. Silent degradation for non-title callers: response_format is now dropped for every caller, not just title_generation. A caller that relied on json_schema enforcement gets an unconstrained reply with no signal that the contract was degraded. The title path tolerates it (documented), but other future callers may not discover the drop. Suggest a debug/warning log when the key is stripped so the degradation is observable.

  2. Scope of the strip: the filter only applies to the extra_body passthrough in _AnthropicCompletionsAdapter.create. Confirm response_format can never arrive as a top-level kwargs argument to create() (the adapter's other inputs) — otherwise the leak persists on another path. A test that passes response_format as a top-level kwarg would close the loop.

  3. The centralized _ANTHROPIC_UNSUPPORTED_EXTRA_BODY_KEYS frozenset and the sibling-preservation test are good — this keeps the exclusion list auditable in one place.

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.
@mmcclean-aws

Copy link
Copy Markdown
Author

Thanks — both actionable points addressed in 75001c5.

1. Silent degradation (fixed). Agreed: dropping response_format quietly downgrades a caller's contract from schema-enforced to best-effort prompt compliance, and the next caller shouldn't have to infer that from a malformed reply. The adapter now logs the drop, naming the keys and the model:

agent.auxiliary_client Anthropic Messages API: dropped unsupported extra_body
keys ['response_format'] for model us.anthropic.claude-opus-5 (no Messages-API
equivalent; structured output degrades to prompt compliance)

Debug rather than warning, deliberately: both current callers (title_generation, plugin_llm's json_mode/json_schema) tolerate the degradation by design, so a warning would fire on every aux call on Bedrock — the exact per-call noise that makes real warnings ignorable. It's guarded by isEnabledFor(DEBUG) so the set intersection is skipped on the hot path. If we later add a caller that genuinely can't tolerate the drop, the right fix is the follow-up below (translate rather than drop), not louder logging.

2. Scope of the strip — checked, and the loop is closed. response_format cannot arrive as a top-level kwargs argument. _AnthropicCompletionsAdapter.create() is allow-list shaped, not passthrough shaped: it pulls a fixed set of OpenAI keys (model, messages, tools, tool_choice, max_tokens/max_completion_tokens, temperature, extra_body, plus the _-prefixed private ones) and hands them to build_anthropic_kwargs, which builds the Messages body from scratch. Anything unrecognized is dropped on the floor — never splatted onward. Empirically, passing it top-level:

build_anthropic_kwargs got response_format: False
api_kwargs keys                            : ['max_tokens', 'messages', 'model']
response_format top-level                  : False
response_format in extra_body              : False

You're right that this shouldn't rest on reading the current code, so I pinned it: test_anthropic_aux_response_format_top_level_kwarg_not_forwarded passes response_format as a top-level kwarg and asserts it reaches neither build_anthropic_kwargs nor the SDK (top-level or smuggled into extra_body). If anyone refactors create() toward **kwargs-splat forwarding, that test fails rather than silently reopening the leak on a second path. Added test_anthropic_aux_logs_dropped_response_format for point 1.

Verification

$ pytest tests/agent/test_auxiliary_client.py -k "response_format or extra_body_passthrough"
5 passed, 180 deselected

The pre-existing test_anthropic_aux_extra_body_passthrough still passes, so legitimate vendor passthrough (thinking, metadata) is unaffected. Re-verified on live Bedrock (us.anthropic.claude-opus-5, us-west-2): the debug line fires and call_llm returns 200 with a parseable title, where before the change the same call raised 400 response_format: Extra inputs are not permitted.

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 response_format into the Messages API's native structured-output mechanism (forced single-tool call, or assistant prefill) would give Anthropic-wire providers real schema enforcement instead of degrading. That's the follow-up noted in the PR description and I'm happy to take it as a separate PR — keeping this one to the minimal change that restores titling.

ethernet8023 added a commit that referenced this pull request Aug 19, 2026
…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>
@ethernet8023

Copy link
Copy Markdown
Collaborator

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:

  • Your live Bedrock parameter-isolation table (400 for json_schema AND json_object, 200 without the field) is what proved retry-with-a-looser-format could never work and the fix had to act at the adapter. That evidence set the direction for the whole cluster.
  • Your review follow-up's top-level-kwarg pin-test ships in the merged branch as commit 56eaf02, with Co-authored-by credit to you.
  • Your "follow-up (not in this PR)" note — translate instead of strip — is exactly what merged: response_format now becomes the Messages API's native output_config.format (Anthropic structured outputs), so plugin complete_structured() keeps schema enforcement instead of degrading to prompt compliance.

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.

lisajlau pushed a commit to lisajlau/hermes-agent that referenced this pull request Aug 20, 2026
…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>
bobaba76 pushed a commit to bobaba76/hermes-agent that referenced this pull request Aug 27, 2026
…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>
melon-xf added a commit to melon-xf/hermes-agent that referenced this pull request Sep 3, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have provider/anthropic Anthropic native Messages API provider/bedrock AWS Bedrock (boto3, IAM) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Auto-title fails 100% on Bedrock/Anthropic: OpenAI-only response_format leaked onto the Messages API (Extra inputs are not permitted)

4 participants