Skip to content

fix(anthropic_adapter): only mangle dots in Claude-shaped model IDs (#17171)#17282

Closed
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/anthropic-adapter-claude-shape-guard-17171
Closed

fix(anthropic_adapter): only mangle dots in Claude-shaped model IDs (#17171)#17282
briandevans wants to merge 1 commit into
NousResearch:mainfrom
briandevans:fix/anthropic-adapter-claude-shape-guard-17171

Conversation

@briandevans

@briandevans briandevans commented Apr 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • normalize_model_name blindly rewrote dots to hyphens for every non-Bedrock model, corrupting non-Anthropic vendor IDs whose dots are version separators (gpt-5.4gpt-5-4, glm-4.7glm-4-7, z-ai/glm-5.1z-ai/glm-5-1, minimax-m2.5-freeminimax-m2-5-free).
  • After the prefix-strip and Bedrock-detection guards, the rewrite is now gated on the model containing claude (case-insensitive). Claude callers are unaffected; non-Claude IDs survive intact even when preserve_dots=False.

The bug

The dot→hyphen rewrite is an Anthropic-Messages-API quirk: OpenRouter form claude-opus-4.6 must become Anthropic form claude-opus-4-6. The function applied it unconditionally to every model except Bedrock IDs, so a non-Claude model accidentally routed through the Anthropic adapter — typically because the caller defaulted to provider=anthropic after failing to infer the right provider — got its name mangled into a non-existent ID and 404'd at the API. The user-facing failure named a model they never typed (gpt-5-4), making the misconfiguration hard to recognize.

The reporter's repro (#17171):

  1. webui session created with {"model": "gpt-5.4"} and no provider hint.
  2. resolve_model_provider("gpt-5.4") returns (gpt-5.4, None, None) — no inference from the gpt-* shape.
  3. resolve_runtime_provider(requested=None) falls back to the default-detected anthropic provider.
  4. AIAgent is built with provider="anthropic", api_mode="anthropic_messages", model="gpt-5.4".
  5. normalize_model_name("gpt-5.4", preserve_dots=False) returns "gpt-5-4".
  6. Request goes to api.anthropic.com/v1/messages with model: gpt-5-4 → HTTP 404.

Same root cause underlies #16417 (custom anthropic_messages providers) and #13061 (zenmux + custom anthropic_messages). The opencode-zen subset (#7421) was previously mitigated by adding the provider to the _anthropic_preserve_dots() allowlist, but the underlying invariant — only Anthropic-shaped names need Anthropic-shaped normalization — was never enforced at the function itself.

The fix

Narrow the rewrite to Claude-shaped IDs:

if not preserve_dots:
    if _is_bedrock_model_id(model):
        return model
    if "claude" not in lower:        # ← new
        return model
    model = model.replace(".", "-")
  • Claude users are unaffected (claude-opus-4.6claude-opus-4-6 still works, mixed case Claude-Opus-4.6 too).
  • The explicit preserve_dots=True flag still short-circuits before the new check, so the documented contract for the alibaba / minimax / opencode-zen / zai / bedrock allowlist is preserved.
  • The default preserve_dots=False path becomes safe for non-Claude IDs that get routed through the Anthropic adapter — they round-trip verbatim instead of being silently corrupted.

Contract Protected

Invariant: the dot→hyphen rewrite in normalize_model_name only fires when both (a) the caller has not opted into preserve_dots=True and (b) the model name shape is Claude-like (contains claude after the optional anthropic/ prefix strip).

Input class Example Before After
Claude bare claude-opus-4.6 claude-opus-4-6 claude-opus-4-6
Claude OpenRouter anthropic/claude-opus-4.6 claude-opus-4-6 claude-opus-4-6
Claude mixed case Claude-Opus-4.6 Claude-Opus-4-6 Claude-Opus-4-6
Bedrock inference profile global.anthropic.claude-opus-4-7 global.anthropic.claude-opus-4-7 global.anthropic.claude-opus-4-7
OpenAI Codex gpt-5.4 gpt-5-4 gpt-5.4 ✅ (#17171)
ZAI glm-4.7 glm-4-7 glm-4.7 ✅ (#16417)
zenmux/GLM z-ai/glm-5.1 z-ai/glm-5-1 z-ai/glm-5.1 ✅ (#13061)
MiniMax MiniMax-M2.7 MiniMax-M2-7 MiniMax-M2.7
Gemini google/gemini-2.5-pro google/gemini-2-5-pro google/gemini-2.5-pro

Test plan

Related

Fixes #17171
Related: #16417, #13061, #7421 (mitigated earlier via _anthropic_preserve_dots() allowlist; this PR enforces the same outcome at the normalization layer for vendors not on that allowlist)

Positioning vs #17290

@vominh1919's #17290 (opened ~12 min after this one) is a narrower fix at the same line. Differences in scope:

Either fix unblocks the reporter's repro on #17171; this PR additionally protects the invariant against future regressions and cleans up the stale test.

…ousResearch#17171)

`normalize_model_name` blindly converted dots to hyphens for any
non-Bedrock model, which corrupted non-Anthropic vendor IDs whose
dots are version separators in their own right. The dot→hyphen
rewrite is an Anthropic-Messages-API quirk (OpenRouter `4.6` →
Anthropic `4-6`); applying it to `gpt-5.4`, `glm-4.7`,
`z-ai/glm-5.1`, or `minimax-m2.5-free` produces non-existent IDs
that the downstream API 404s on, with an error message that names
a model the user never typed.

The reporter's repro (NousResearch#17171): webui's `resolve_model_provider`
fails to infer `openai-codex` from a `gpt-*` model name, falls
back to the default Anthropic provider, and the request reaches
the Messages API as `gpt-5-4`. Same root cause as NousResearch#16417
(custom anthropic_messages providers) and NousResearch#13061 (zenmux +
custom anthropic_messages); the opencode-zen subset (NousResearch#7421) was
mitigated earlier via the `_anthropic_preserve_dots()` allowlist
but the underlying invariant was never enforced.

Narrow the rewrite to Claude-shaped IDs: after the prefix-strip
and Bedrock-detection guards, skip mangling when the model name
has no `claude` substring. Claude callers are unaffected; the
explicit `preserve_dots=True` flag remains the documented contract
for known non-Claude allowlist providers and now matches the
default behavior for unlisted vendors.

Test plan
- Parametrized regression: gpt-5.4, glm-4.7, qwen3.5-plus,
  z-ai/glm-5.1, minimax-m2.5-free, openai/gpt-5.4,
  google/gemini-2.5-pro, gemini-2.5-pro, deepseek-v3.1
- Negative invariant: claude-opus-4.6 → claude-opus-4-6 still
  mangled; case-insensitive Claude detection covers
  `Claude-Opus-4.6`
- Reporter repro (NousResearch#17171): `normalize_model_name("gpt-5.4")` and
  `normalize_model_name("anthropic/gpt-5.4")` both round-trip
- `preserve_dots=True` short-circuits before the new check
- Updated `test_minimax_provider.py::test_normalize_converts_without_preserve`
  — the assertion previously documented the bug; it now documents
  the fix (MiniMax-M2.7 survives even without preserve_dots=True)
- Bedrock integration suite (55 tests) and Anthropic-adapter tests
  unrelated to OAuth credentials all pass
- Regression guard: reverting the production change makes 10/12
  invariant tests fail with the expected `gpt-5-4`/`MiniMax-M2-7`
  outputs

Fixes NousResearch#17171
Related: NousResearch#16417, NousResearch#13061, NousResearch#7421

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings April 29, 2026 05:17

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes model-name normalization in the Anthropic adapter so that dot→hyphen rewriting only applies to Claude-shaped model IDs, preventing accidental corruption of non-Anthropic vendor model IDs (e.g., gpt-5.4, glm-4.7) when they are routed through the Anthropic Messages path.

Changes:

  • Gate dot→hyphen rewriting in normalize_model_name behind a case-insensitive "claude" check (while still preserving Bedrock IDs).
  • Add/expand regression tests asserting non-Claude dotted IDs round-trip unchanged with default preserve_dots=False.
  • Update the MiniMax provider test to reflect the new invariant (non-Claude IDs no longer require preserve_dots=True to avoid mangling).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
agent/anthropic_adapter.py Restricts dot→hyphen normalization to Claude-shaped IDs and refreshes lower after stripping anthropic/.
tests/agent/test_anthropic_adapter.py Adds invariant/regression coverage for non-Claude dotted model IDs and ensures Claude dotted IDs still normalize.
tests/agent/test_minimax_provider.py Updates MiniMax normalization test to assert dots are preserved even without preserve_dots=True for non-Claude IDs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent loop, run_agent.py, prompt builder labels Apr 29, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Competing PR with #17290 — both fix normalize_model_name() dot-mangling for non-Claude models (#17171).

@briandevans

Copy link
Copy Markdown
Contributor Author

@alt-glitch Thanks for the cross-link. Quick comparison vs #17290 — both fixes target the same line in normalize_model_name(), but they're not interchangeable:

Aspect This PR (#17282) #17290
Filed at 05:17:58Z 05:30:00Z (12 min later)
Guard if "claude" not in lower: return model (substring, runs after anthropic/ strip — see re-lower() on the stripped name) _lower.startswith("claude-") or _lower.startswith("anthropic/") (the second arm is unreachable since anthropic/ has already been stripped at this point)
Tests added 12 new tests across TestNormalizeModelNameNonClaudeInvariant — parametrized for gpt-5.4, glm-4.7, qwen3.5-plus, z-ai/glm-5.1, minimax-m2.5-free, openai/gpt-5.4, google/gemini-2.5-pro, gemini-2.5-pro, deepseek-v3.1; negative claude still mangled; mixed-case Claude-Opus-4.6; preserve_dots=True still authoritative 0 new tests
Regression coverage cited #17171, #16417, #13061, #7421 (each issue has at least one corresponding parametrized case) #17171 only
Stale test fixed tests/agent/test_minimax_provider.py::test_normalize_converts_without_preserve previously asserted MiniMax-M2-7 ("broken for MiniMax" per its own docstring) and would silently keep documenting the bug under #17290's fix — this PR rewrites it as test_normalize_preserves_dots_for_non_claude_without_preserve_flag and asserts the correct MiniMax-M2.7 not addressed

Happy to defer if a maintainer prefers the narrower allowlist shape — but the parametrized invariant + the stale-test fix are the parts I'd want to preserve either way.

CI audit — the single test job failure on commit c30208242 is a pre-existing baseline on clean origin/main. Zero failures intersect with touched code.

Test Symptom Root cause on main
tests/agent/test_anthropic_adapter.py::TestBuildAnthropicClient::test_custom_base_url default_headers["anthropic-beta"] now also contains context-1m-2025-08-07; the equality assertion in the test wasn't updated when the beta was added to build_anthropic_client reproduces verbatim with git checkout origin/main -- tests/agent/test_anthropic_adapter.py agent/anthropic_adapter.py && pytest tests/agent/test_anthropic_adapter.py::TestBuildAnthropicClient::test_custom_base_url — fails with the same context-1m-2025-08-07 diff

The TestNormalizeModelNameNonClaudeInvariant block (12/12) and the TestMiniMaxNormalizationDots block both pass locally on this branch.

@briandevans

Copy link
Copy Markdown
Contributor Author

Closing — superseded by @vominh1919's fix that landed on main in 7141cda9.

Quick honest comparison:

Aspect This PR (#17282) Landed on main (7141cda9)
Guard if "claude" not in lower: return model if _lower.startswith("claude-") or _lower.startswith("anthropic/"): model = model.replace(".", "-")
Coverage Same root cause as #17171 / #16417 / #13061 / #7421 Same
Test footprint 9 parametrized non-Claude IDs + repro + Claude negative none added

The landed version is more conservativestartswith("claude-") won't accidentally apply to a hypothetical non-Claude ID that happens to contain the substring claude (e.g., claude-but-actually-qwen-3.5). My substring guard would short-circuit such a name; the startswith guard is the right shape. I should have used that.

Thanks @vominh1919 — happy to see this land. Closing here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent loop, run_agent.py, prompt builder P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

openai-codex fallback sends 'gpt-5-4' instead of 'gpt-5.4' to Codex backend, returning HTTP 404

3 participants