Skip to content

fix(anthropic): honor custom apiModelId instead of silently defaulting to claude-sonnet-4-5#842

Merged
navedmerchant merged 5 commits into
Zoo-Code-Org:mainfrom
grizmin:fix/anthropic-custom-model-id-fallback
Jul 9, 2026
Merged

fix(anthropic): honor custom apiModelId instead of silently defaulting to claude-sonnet-4-5#842
navedmerchant merged 5 commits into
Zoo-Code-Org:mainfrom
grizmin:fix/anthropic-custom-model-id-fallback

Conversation

@grizmin

@grizmin grizmin commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • AnthropicHandler.getModel() coerced any apiModelId not present in the static anthropicModels table down to anthropicDefaultModelId ("claude-sonnet-4-5") — and that coerced id is what actually got sent as model in the API request, silently ignoring a user-configured custom model name.
  • The same fallback broke capability lookups used to build the thinking request param: an unrecognized id inherited the default model's (older) capability profile, so custom deployments of newer models (e.g. Sonnet 5 class) got the legacy thinking: {type: "enabled", budget_tokens} shape instead of {type: "adaptive"}, which those models reject with a 400.
  • Fix: the model id sent to the API always honors a user-configured apiModelId. For unrecognized values, capabilities are best-effort guessed via known model-family substring matching (same pattern as the existing BedrockHandler.guessModelInfoFromId).

Fixes #418
Fixes #843

Test plan

  • Added regression tests in anthropic.spec.ts covering: custom model id preserved in getModel().id, capability guessing via substring match, fallback to default info when no family matches, and createMessage sending the raw custom id with thinking: {type: "adaptive"}.
  • pnpm --filter zoo-code test - 50/50 passing
  • pnpm --filter zoo-code check-types - clean
  • pnpm --filter zoo-code lint - clean

Summary by CodeRabbit

  • Bug Fixes
    • Custom Anthropic model IDs are now honored as provided, instead of being silently replaced with a default model.
    • When an ID isn’t recognized, model capability details used for reasoning/thinking are now determined via best-effort family matching for more reliable behavior.
    • If the ID matches no known family, the request still uses a sensible fallback model contract while preserving the chosen model ID.
  • Tests
    • Added coverage for custom/unrecognized Anthropic model IDs and reasoning-related request parameters.

grizmin added 3 commits July 6, 2026 15:06
…g to claude-sonnet-4-5

Unrecognized model IDs were coerced to the hardcoded default before being sent to the API and for capability lookups, breaking custom deployments and picking the wrong thinking config.

Fixes Zoo-Code-Org#418
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 06d113f4-362a-4d8a-8cec-bb423a4d4ae1

📥 Commits

Reviewing files that changed from the base of the PR and between aaf2862 and 7dcf6db.

📒 Files selected for processing (1)
  • src/api/providers/anthropic.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/api/providers/anthropic.ts

📝 Walkthrough

Walkthrough

The Anthropic provider now preserves unrecognized apiModelId values instead of replacing them with the default model. It guesses model capabilities from known model-family substrings when needed, with tests and a changeset added for the behavior.

Changes

Custom apiModelId fallback fix

Layer / File(s) Summary
Model ID preservation and capability guessing implementation
src/api/providers/anthropic.ts
Adds case-insensitive lookup helpers for known Anthropic model IDs, implements substring-based guessModelInfoFromId, and updates getModel() to preserve custom apiModelId values while resolving info from known models or the new fallback heuristic.
Tests and changeset for fallback fix
src/api/providers/__tests__/anthropic.spec.ts, .changeset/fix-anthropic-custom-model-id-fallback.md
Adds createMessage and getModel coverage for custom model ID preservation, inferred capabilities, and default-info fallback when no family matches, plus a changeset describing the fix.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant AnthropicHandler
  participant guessModelInfoFromId
  participant AnthropicSDK

  AnthropicHandler->>AnthropicHandler: resolve apiModelId
  alt custom id is known
    AnthropicHandler->>AnthropicHandler: use anthropicModels[id]
  else custom id is unknown
    AnthropicHandler->>guessModelInfoFromId: infer ModelInfo from modelId
    guessModelInfoFromId-->>AnthropicHandler: guessed info or default info
  end
  AnthropicHandler->>AnthropicSDK: send request with preserved model id and thinking shape
Loading

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#778: Adds Anthropic Sonnet 5 model metadata that this PR’s substring-based guessing logic can match against.

Suggested reviewers: hannesrudolph, edelauna, navedmerchant

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: honoring custom Anthropic apiModelId values instead of defaulting to claude-sonnet-4-5.
Description check ✅ Passed The description includes the bug summary, linked issues, implementation approach, and a test plan, which is mostly complete for this repo.
Linked Issues check ✅ Passed The changes match #418 and #843 by preserving custom apiModelId values and guessing capabilities to produce the correct thinking shape.
Out of Scope Changes check ✅ Passed The PR stays focused on Anthropic model-id fallback and capability inference, with matching tests and changeset only.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{}

@github-actions github-actions Bot added the awaiting-review PR changes are ready and waiting for maintainer re-review label Jul 6, 2026

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

Thanks for this fix! Couple of small comments and should be good to merge

Comment thread src/api/providers/anthropic.ts Outdated
// Guesses capabilities for an unrecognized model ID via known-family substring match.
private guessModelInfoFromId(modelId: string): ModelInfo {
const matchedId = (Object.keys(anthropicModels) as AnthropicModelId[])
.sort((a, b) => b.length - a.length)

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.

lets hoist a sorted list out of this method so we dont have to needlessly sort every time

Comment thread src/api/providers/anthropic.ts Outdated
private guessModelInfoFromId(modelId: string): ModelInfo {
const matchedId = (Object.keys(anthropicModels) as AnthropicModelId[])
.sort((a, b) => b.length - a.length)
.find((knownId) => modelId.includes(knownId))

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.

we should lowercase the modelId before matching so the matching is case insensitve

@coderabbitai coderabbitai Bot 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.

♻️ Duplicate comments (1)
src/api/providers/anthropic.ts (1)

356-363: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Substring matching is case-sensitive — custom IDs with non-lowercase casing won't match known families.

modelId.includes(knownId) is case-sensitive, so a custom deployment ID like Claude-Sonnet-5-BF would fail to match claude-sonnet-5 and fall back to the default model's capabilities instead of the correct Sonnet 5 family. This was flagged in a previous review and remains unaddressed.

🛡️ Proposed fix for case-insensitive matching
 private guessModelInfoFromId(modelId: string): ModelInfo {
-	const matchedId = (Object.keys(anthropicModels) as AnthropicModelId[])
-		.sort((a, b) => b.length - a.length)
-		.find((knownId) => modelId.includes(knownId))
+	const lowerModelId = modelId.toLowerCase()
+	const matchedId = (Object.keys(anthropicModels) as AnthropicModelId[])
+		.sort((a, b) => b.length - a.length)
+		.find((knownId) => lowerModelId.includes(knownId))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/anthropic.ts` around lines 356 - 363, The capability lookup
in guessModelInfoFromId is still using case-sensitive substring matching, so
custom model IDs with different casing can miss the right family and fall back
to the default. Update the matching logic in anthopicModels lookup to compare
normalized casing for both modelId and each knownId before calling includes,
while keeping the longest-match ordering and returning the same ModelInfo
fallback behavior.
🧹 Nitpick comments (1)
src/api/providers/anthropic.ts (1)

365-374: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Custom model IDs now bypass prompt caching and 1M context beta in createMessage.

getModel() correctly preserves the custom apiModelId, but createMessage (lines 91–228) and the 1M context check (lines 76–84) match on the raw modelId via switch/case against known IDs only. A custom ID like claude-sonnet-5-bf falls to the default case, which omits cache_control breakpoints and the prompt-caching-2024-07-31 beta header. This isn't a regression (the old behavior sent the wrong model and failed), but it's a missed optimization for custom deployments of caching-capable models.

Consider deriving caching decisions from the guessed model family rather than the raw custom ID, either now or as a follow-up.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/api/providers/anthropic.ts` around lines 365 - 374,
`AnthropicProvider.createMessage` and the 1M-context gating currently branch on
the raw `modelId`, so custom `apiModelId` values skip prompt caching and the
`prompt-caching-2024-07-31` beta even when they map to a caching-capable Claude
family. Update the model-selection logic to derive these feature decisions from
the resolved/guessed model info returned by `getModel()` or
`guessModelInfoFromId`, and use that in the switch/case and 1M-context check
instead of only matching known IDs. Keep custom IDs preserved, but ensure
`createMessage` still enables cache breakpoints and beta headers when the
guessed family supports them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@src/api/providers/anthropic.ts`:
- Around line 356-363: The capability lookup in guessModelInfoFromId is still
using case-sensitive substring matching, so custom model IDs with different
casing can miss the right family and fall back to the default. Update the
matching logic in anthopicModels lookup to compare normalized casing for both
modelId and each knownId before calling includes, while keeping the
longest-match ordering and returning the same ModelInfo fallback behavior.

---

Nitpick comments:
In `@src/api/providers/anthropic.ts`:
- Around line 365-374: `AnthropicProvider.createMessage` and the 1M-context
gating currently branch on the raw `modelId`, so custom `apiModelId` values skip
prompt caching and the `prompt-caching-2024-07-31` beta even when they map to a
caching-capable Claude family. Update the model-selection logic to derive these
feature decisions from the resolved/guessed model info returned by `getModel()`
or `guessModelInfoFromId`, and use that in the switch/case and 1M-context check
instead of only matching known IDs. Keep custom IDs preserved, but ensure
`createMessage` still enables cache breakpoints and beta headers when the
guessed family supports them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 77b292ba-936a-4be7-a501-4f8d4bcec7ae

📥 Commits

Reviewing files that changed from the base of the PR and between edb6564 and aaf2862.

📒 Files selected for processing (3)
  • .changeset/fix-anthropic-custom-model-id-fallback.md
  • src/api/providers/__tests__/anthropic.spec.ts
  • src/api/providers/anthropic.ts

@navedmerchant navedmerchant added this pull request to the merge queue Jul 9, 2026
Merged via the queue into Zoo-Code-Org:main with commit 13c803b Jul 9, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting-review PR changes are ready and waiting for maintainer re-review

Projects

None yet

2 participants