Skip to content

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

Open
grizmin wants to merge 4 commits into
Zoo-Code-Org:mainfrom
grizmin:fix/anthropic-custom-model-id-fallback
Open

fix(anthropic): honor custom apiModelId instead of silently defaulting to claude-sonnet-4-5#842
grizmin wants to merge 4 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

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 preserved instead of being silently replaced with a default model.
    • Model capabilities are now estimated more accurately for unknown model IDs, helping reasoning-related requests behave correctly.
    • If a custom model doesn’t match a known family, the app still uses a sensible fallback while keeping the chosen model ID.

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

📝 Walkthrough

Walkthrough

The Anthropic provider's getModel() no longer replaces an unrecognized apiModelId with the hardcoded default model. A new guessModelInfoFromId helper infers capabilities via substring matching against known model families, falling back to default info only when no match is found. Tests and a changeset accompany the fix.

Changes

Custom apiModelId fallback fix

Layer / File(s) Summary
Model ID preservation and capability guessing
src/api/providers/anthropic.ts
Adds guessModelInfoFromId helper matching unknown model IDs to known models via substring inclusion, and reworks getModel() to always honor the configured apiModelId, deriving ModelInfo from the guess helper when the ID is unknown instead of defaulting to anthropicDefaultModelId.
Tests and changeset
src/api/providers/__tests__/anthropic.spec.ts, .changeset/fix-anthropic-custom-model-id-fallback.md
Adds createMessage and getModel tests validating custom model ID preservation, family-based capability inference, and fallback to default info when no family matches; adds a changeset documenting the fix.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant AnthropicHandler
  participant guessModelInfoFromId
  participant AnthropicSDK

  User->>AnthropicHandler: getModel() with custom apiModelId
  AnthropicHandler->>AnthropicHandler: check if id in anthropicModels
  alt id known
    AnthropicHandler->>AnthropicHandler: use anthropicModels[id]
  else id unknown
    AnthropicHandler->>guessModelInfoFromId: guess(id)
    guessModelInfoFromId-->>AnthropicHandler: matched family info or default info
  end
  AnthropicHandler-->>User: model {id, info}
  User->>AnthropicHandler: createMessage()
  AnthropicHandler->>AnthropicSDK: send request with configured id and thinking shape
Loading

Possibly related PRs

  • Zoo-Code-Org/Zoo-Code#317: Updates a provider getModel() to stop silently replacing an unrecognized apiModelId with the provider default, analogous to this PR's Anthropic fix.
  • Zoo-Code-Org/Zoo-Code#555: Extends anthropicModels with a new entry, directly affecting the substring-matching guess/fallback logic changed here.

Suggested reviewers: hannesrudolph, edelauna, navedmerchant, JamesRobert20

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the main change: preserving custom Anthropic apiModelId values instead of defaulting to claude-sonnet-4-5.
Description check ✅ Passed The description covers the linked issues, the fix approach, and test results; only the repository's full template headings are missing.
Linked Issues check ✅ Passed The code and tests match both linked bugs: custom model ids are preserved, and unknown ids get best-effort capability guessing for correct thinking.
Out of Scope Changes check ✅ Passed The changes stay focused on Anthropic model-id handling and regression tests, with no unrelated functionality introduced.
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

// 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

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

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