feat: Add Claude Sonnet 4.6 model support to Anthropic provider (#13016)

<!-- Template from
https://github.com/kubevirt/kubevirt/blob/main/.github/PULL_REQUEST_TEMPLATE.md?-->
<!--  Thanks for sending a pull request!  Here are some tips for you:
1. Consider creating this PR as draft:
https://github.com/CherryHQ/cherry-studio/blob/main/CONTRIBUTING.md
-->

<!--

⚠️ Important: Redux/IndexedDB Data-Changing Feature PRs Temporarily On
Hold ⚠️

Please note: For our current development cycle, we are not accepting
feature Pull Requests that introduce changes to Redux data models or
IndexedDB schemas.

While we value your contributions, PRs of this nature will be blocked
without merge. We welcome all other contributions (bug fixes, perf
enhancements, docs, etc.). Thank you!

Once version 2.0.0 is released, we will resume reviewing feature PRs.

-->

### What this PR does

Before this PR:
- Lack of Sonnet 4.6 related configuration support, not fully available

After this PR:
- Added Claude Sonnet 4.6 (`claude-sonnet-4-6`) to the Anthropic
provider
- Added support for reasoning effort (extended thinking) on Claude
Sonnet 4.6 models
- Added proper regex patterns to detect all Claude 4.6 series models
(Opus, Sonnet, Haiku)
- Renamed model detection functions to reflect the entire Claude 4.6
series

<!-- (optional, in `fixes #<issue number>(, fixes #<issue_number>, ...)`
format, will close the issue(s) when PR gets merged)*: -->

Fixes #

### Why we need it and why it was done in this way

The Claude 4.6 series introduces new models beyond Opus, including
Sonnet 4.6 with reasoning capabilities. This PR ensures Cherry Studio
fully supports the new Sonnet 4.6 model with proper model detection,
token limits, and reasoning effort configuration.

The following tradeoffs were made:
- Used regex patterns that match the entire Claude 4.6 series for future
extensibility
- Maintained backward compatibility with existing Opus 4.6 model
detection

The following alternatives were considered:
- Separate detection for each model type (rejected in favor of unified
series detection)
- Hardcoding model IDs instead of regex patterns (rejected for
maintainability)

Links to places where the discussion took place: <!-- optional: slack,
other GH issue, mailinglist, ... -->

### Breaking changes

<!-- optional -->

If this PR introduces breaking changes, please describe the changes and
the impact on users.

N/A

### Special notes for your reviewer

<!-- optional -->

This PR includes refactoring commits that rename `opus46` to `claude46`
for model type detection. The logic remains the same but now applies to
the entire Claude 4.6 series.

### Checklist

This checklist is not enforcing, but it's a reminder of items that could
be relevant to every PR.
Approvers are expected to review this list.

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: [Write code that humans can
understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans)
and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle)
- [x] Refactor: You have [left the code cleaner than you found it (Boy
Scout
Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A [user-guide update](https://docs.cherry-ai.com)
was considered and is present (link) or not required. You want a
user-guide update if it's a user facing feature.

### Release note

<!--  Write your release note:
1. Enter your extended release note in the below block. If the PR
requires additional action from users switching to the new release,
include the string "action required".
2. If no release note is required, just write "NONE".
-->

```release-note
feat: Add Claude Sonnet 4.6 model support to Anthropic provider with reasoning effort configuration
```
This commit is contained in:
Phantom
2026-02-24 20:03:10 +08:00
committed by GitHub
parent 1460209aa8
commit 7cca23571a
7 changed files with 111 additions and 62 deletions

View File

@@ -9,6 +9,7 @@ import {
findTokenLimit,
GEMINI_FLASH_MODEL_REGEX,
getModelSupportedReasoningEffortOptions,
isClaude46SeriesModel,
isDeepSeekHybridInferenceModel,
isDoubaoSeed18Model,
isDoubaoSeedAfter251015,
@@ -18,7 +19,6 @@ import {
isOpenAIDeepResearchModel,
isOpenAIModel,
isOpenAIReasoningModel,
isOpus46Model,
isQwenAlwaysThinkModel,
isQwenReasoningModel,
isReasoningModel,
@@ -559,7 +559,7 @@ export function getAnthropicThinkingBudget(
* Extracted from AnthropicAPIClient logic.
*
* Returns different parameter shapes depending on the model:
* - **Opus 4.6**: `{ thinking: { type: 'adaptive' }, effort: 'low' | 'medium' | 'high' | 'max' }`
* - **Claude 4.6**: `{ thinking: { type: 'adaptive' }, effort: 'low' | 'medium' | 'high' | 'max' }`
* Uses the new adaptive thinking API with effort-based control.
* - **Other Claude models** (4.0, 4.1, 4.5, etc.): `{ thinking: { type: 'enabled', budgetTokens: number } }`
* Uses the classic thinking API with explicit token budget.
@@ -588,10 +588,10 @@ export function getAnthropicReasoningParams(
// Claude reasoning parameters
if (isSupportedThinkingTokenClaudeModel(model)) {
// Opus 4.6 uses adaptive thinking + effort parameters
// Map reasoningEffort to Opus 4.6 supported effort values
if (isOpus46Model(model)) {
// Opus 4.6 supports: low, medium, high, max
// Claude 4.6 uses adaptive thinking + effort parameters
// Map reasoningEffort to Claude 4.6 supported effort values
if (isClaude46SeriesModel(model)) {
// Claude 4.6 supports: low, medium, high, max
// Mapping rules: default/none -> no effort (uses default high)
// minimal/low -> low
// medium -> medium
@@ -772,8 +772,8 @@ export function getBedrockReasoningParams(
return {}
}
// Opus 4.6 uses adaptive thinking + maxReasoningEffort
if (isOpus46Model(model)) {
// Claude 4.6 uses adaptive thinking + maxReasoningEffort
if (isClaude46SeriesModel(model)) {
const effortMap = {
auto: undefined,
minimal: 'low',

View File

@@ -2379,26 +2379,27 @@ describe('isInterleavedThinkingModel', () => {
})
describe('Claude Models', () => {
describe('getThinkModelType for Opus 4.6', () => {
it('should return opus46 for Opus 4.6 models', () => {
expect(getThinkModelType(createModel({ id: 'claude-opus-4-6' }))).toBe('opus46')
expect(getThinkModelType(createModel({ id: 'anthropic.claude-opus-4-6-v1' }))).toBe('opus46')
describe('getThinkModelType for Claude 4.6 series models', () => {
it('should return claude46 for Claude 4.6 models', () => {
expect(getThinkModelType(createModel({ id: 'claude-opus-4-6' }))).toBe('claude46')
expect(getThinkModelType(createModel({ id: 'claude-sonnet-4-6' }))).toBe('claude46')
expect(getThinkModelType(createModel({ id: 'anthropic.claude-opus-4-6-v1' }))).toBe('claude46')
})
})
describe('MODEL_SUPPORTED_OPTIONS for Opus 4.6', () => {
it('should have correct options for opus46', () => {
expect(MODEL_SUPPORTED_OPTIONS.opus46).toEqual(['default', 'none', 'low', 'medium', 'high', 'xhigh'])
describe('MODEL_SUPPORTED_OPTIONS for Claude 4.6', () => {
it('should have correct options for claude46', () => {
expect(MODEL_SUPPORTED_OPTIONS.claude46).toEqual(['default', 'none', 'low', 'medium', 'high', 'xhigh'])
})
})
describe('MODEL_SUPPORTED_REASONING_EFFORT for Opus 4.6', () => {
it('should have correct effort levels for opus46', () => {
expect(MODEL_SUPPORTED_REASONING_EFFORT.opus46).toEqual(['low', 'medium', 'high', 'xhigh'])
describe('MODEL_SUPPORTED_REASONING_EFFORT for Claude 4.6', () => {
it('should have correct effort levels for claude46', () => {
expect(MODEL_SUPPORTED_REASONING_EFFORT.claude46).toEqual(['low', 'medium', 'high', 'xhigh'])
})
})
describe('getModelSupportedReasoningEffortOptions for Opus 4.6', () => {
describe('getModelSupportedReasoningEffortOptions for Claude 4.6', () => {
it('should return correct options for Opus 4.6 models', () => {
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'claude-opus-4-6' }))).toEqual([
'default',
@@ -2409,9 +2410,19 @@ describe('Claude Models', () => {
'xhigh'
])
})
it('should return correct options for Sonnet 4.6 models', () => {
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'claude-sonnet-4-6' }))).toEqual([
'default',
'none',
'low',
'medium',
'high',
'xhigh'
])
})
})
describe('findTokenLimit for Opus 4.6', () => {
describe('findTokenLimit for Claude 4.6', () => {
it('should return 128K max tokens for Opus 4.6 models', () => {
expect(findTokenLimit('claude-opus-4-6')).toEqual({ min: 1024, max: 128_000 })
expect(findTokenLimit('claude-opus-4.6')).toEqual({ min: 1024, max: 128_000 })
@@ -2419,6 +2430,12 @@ describe('Claude Models', () => {
expect(findTokenLimit('claude-opus-4-6@20251201')).toEqual({ min: 1024, max: 128_000 })
})
it('should return 64K max tokens for Sonnet 4.6 models', () => {
expect(findTokenLimit('claude-sonnet-4-6')).toEqual({ min: 1024, max: 64_000 })
expect(findTokenLimit('claude-sonnet-4.6')).toEqual({ min: 1024, max: 64_000 })
expect(findTokenLimit('anthropic.claude-sonnet-4-6')).toEqual({ min: 1024, max: 64_000 })
})
it('should distinguish Opus 4.6 from other Claude models', () => {
// Opus 4.5 should have 64K
expect(findTokenLimit('claude-opus-4-5')).toEqual({ min: 1024, max: 64_000 })

View File

@@ -20,6 +20,7 @@ import {
getModelSupportedVerbosity,
groupQwenModels,
isAnthropicModel,
isClaude46SeriesModel,
isGemini3FlashModel,
isGemini3ProModel,
isGeminiModel,
@@ -28,7 +29,6 @@ import {
isMaxTemperatureOneModel,
isNotSupportSystemMessageModel,
isNotSupportTextDeltaModel,
isOpus46Model,
isSupportedFlexServiceTier,
isSupportedModel,
isSupportFlexServiceTierModel,
@@ -674,58 +674,78 @@ describe('model utils', () => {
})
})
describe('Opus 4.6 Model Detection', () => {
describe('isOpus46Model', () => {
describe('Claude 4.6 Models Detection', () => {
describe('isClaude46SeriesModel', () => {
it('detects Opus 4.6 in direct API format', () => {
expect(isOpus46Model(createModel({ id: 'claude-opus-4-6' }))).toBe(true)
expect(isOpus46Model(createModel({ id: 'claude-opus-4.6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4-6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4.6' }))).toBe(true)
})
it('detects Opus 4.6 with version suffixes', () => {
expect(isOpus46Model(createModel({ id: 'claude-opus-4-6-20251201' }))).toBe(true)
expect(isOpus46Model(createModel({ id: 'claude-opus-4.6-20251201' }))).toBe(true)
expect(isOpus46Model(createModel({ id: 'claude-opus-4-6-preview' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4-6-20251201' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4.6-20251201' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4-6-preview' }))).toBe(true)
})
it('detects Opus 4.6 in AWS Bedrock format', () => {
expect(isOpus46Model(createModel({ id: 'anthropic.claude-opus-4-6-v1' }))).toBe(true)
expect(isOpus46Model(createModel({ id: 'anthropic.claude-opus-4-6-v2:0' }))).toBe(true)
})
it('detects Opus 4.6 in GCP Vertex AI format', () => {
expect(isOpus46Model(createModel({ id: 'claude-opus-4-6@20251201' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'anthropic.claude-opus-4-6-v1' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'anthropic.claude-opus-4-6-v2:0' }))).toBe(true)
})
it('detects Opus 4.6 with provider prefix', () => {
expect(isOpus46Model(createModel({ id: 'anthropic/claude-opus-4-6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'anthropic/claude-opus-4-6' }))).toBe(true)
})
it('handles case insensitivity', () => {
expect(isOpus46Model(createModel({ id: 'CLAUDE-OPUS-4-6' }))).toBe(true)
expect(isOpus46Model(createModel({ id: 'Claude-Opus-4.6' }))).toBe(true)
expect(isOpus46Model(createModel({ id: 'Anthropic.Claude-Opus-4-6-V1' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'CLAUDE-OPUS-4-6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'Claude-Opus-4.6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'Anthropic.Claude-Opus-4-6-V1' }))).toBe(true)
})
it('returns false for other Claude models', () => {
expect(isOpus46Model(createModel({ id: 'claude-opus-4-5' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-opus-4.5' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-opus-4' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-opus-4-0' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-opus-4-1' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-sonnet-4-6' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-3-opus' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'claude-3.5-sonnet' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4-5' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4.5' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4-0' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-opus-4-1' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-3-opus' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'claude-3.5-sonnet' }))).toBe(false)
})
it('detects Sonnet 4.6 in direct API format', () => {
expect(isClaude46SeriesModel(createModel({ id: 'claude-sonnet-4-6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-sonnet-4.6' }))).toBe(true)
})
it('detects Sonnet 4.6 with version suffixes', () => {
expect(isClaude46SeriesModel(createModel({ id: 'claude-sonnet-4-6-20251201' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-sonnet-4.6-20251201' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'claude-sonnet-4-6-preview' }))).toBe(true)
})
it('detects Sonnet 4.6 in AWS Bedrock format', () => {
expect(isClaude46SeriesModel(createModel({ id: 'anthropic.claude-sonnet-4-6' }))).toBe(true)
})
it('detects Sonnet 4.6 with provider prefix', () => {
expect(isClaude46SeriesModel(createModel({ id: 'anthropic/claude-sonnet-4-6' }))).toBe(true)
})
it('handles case insensitivity for Sonnet', () => {
expect(isClaude46SeriesModel(createModel({ id: 'CLAUDE-SONNET-4-6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'Claude-Sonnet-4.6' }))).toBe(true)
expect(isClaude46SeriesModel(createModel({ id: 'Anthropic.Claude-Sonnet-4-6-V1' }))).toBe(true)
})
it('returns false for non-Claude models', () => {
expect(isOpus46Model(createModel({ id: 'gpt-4o' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'gemini-pro' }))).toBe(false)
expect(isOpus46Model(createModel({ id: 'qwen-max' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'gpt-4o' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'gemini-pro' }))).toBe(false)
expect(isClaude46SeriesModel(createModel({ id: 'qwen-max' }))).toBe(false)
})
it('returns false for undefined and null', () => {
expect(isOpus46Model(undefined as unknown as Model)).toBe(false)
expect(isOpus46Model(null as unknown as Model)).toBe(false)
expect(isClaude46SeriesModel(undefined as unknown as Model)).toBe(false)
expect(isClaude46SeriesModel(null as unknown as Model)).toBe(false)
})
})
})

View File

@@ -379,6 +379,12 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
name: 'Claude Opus 4.6',
group: 'Claude 4.6'
},
{
id: 'claude-sonnet-4-6',
provider: 'anthropic',
name: 'Claude Sonnet 4.6',
group: 'Claude 4.6'
},
{
id: 'claude-sonnet-4-5',
provider: 'anthropic',

View File

@@ -22,10 +22,10 @@ import {
} from './openai'
import {
GEMINI_FLASH_MODEL_REGEX,
isClaude46SeriesModel,
isGemini3FlashModel,
isGemini3ProModel,
isKimi25Model,
isOpus46Model,
withModelIdAndNameAsId
} from './utils'
import { isTextToImageModel } from './vision'
@@ -66,8 +66,8 @@ export const MODEL_SUPPORTED_REASONING_EFFORT = {
perplexity: ['low', 'medium', 'high'] as const,
deepseek_hybrid: ['auto'] as const,
kimi_k2_5: ['none', 'auto'] as const,
// Opus 4.6 supports low, medium, high, xhigh (xhigh is mapped to max in API)
opus46: ['low', 'medium', 'high', 'xhigh'] as const
// Claude 4.6 supports low, medium, high, xhigh (xhigh is mapped to max in API)
claude46: ['low', 'medium', 'high', 'xhigh'] as const
} as const satisfies ReasoningEffortConfig
// Model type to supported options mapping
@@ -101,7 +101,7 @@ export const MODEL_SUPPORTED_OPTIONS: ThinkingOptionConfig = {
perplexity: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.perplexity] as const,
deepseek_hybrid: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.deepseek_hybrid] as const,
kimi_k2_5: ['default', ...MODEL_SUPPORTED_REASONING_EFFORT.kimi_k2_5] as const,
opus46: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.opus46] as const
claude46: ['default', 'none', ...MODEL_SUPPORTED_REASONING_EFFORT.claude46] as const
} as const
// TODO: add ut
@@ -176,8 +176,8 @@ const _getThinkModelType = (model: Model): ThinkingModelType => {
thinkingModelType = 'mimo'
} else if (isSupportedThinkingTokenKimiModel(model)) {
thinkingModelType = 'kimi_k2_5'
} else if (isOpus46Model(model)) {
thinkingModelType = 'opus46'
} else if (isClaude46SeriesModel(model)) {
thinkingModelType = 'claude46'
}
return thinkingModelType
}
@@ -765,9 +765,13 @@ const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
// Claude models (supports AWS Bedrock 'anthropic.' prefix, GCP Vertex AI '@' separator, and '-v1:0' suffix)
// Opus 4.6 supports 128K output tokens
'(?:anthropic\\.)?claude-opus-4[.-]6(?:[@\\-:][\\w\\-:]+)?$': { min: 1024, max: 128_000 },
'(?:anthropic\\.)?claude-3[.-]7.*sonnet.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
// Sonnet 4.6, and Haiku is assumed to be also 64k
'(?:anthropic\\.)?claude-(:?sonnet|haiku)-4[.-]6.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
// 4.5 series
'(?:anthropic\\.)?claude-(:?haiku|sonnet|opus)-4[.-]5.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
// Opus 4.1
'(?:anthropic\\.)?claude-opus-4[.-]1.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 32_000 },
// 4.0 series
'(?:anthropic\\.)?claude-sonnet-4(?:[.-]0)?(?:[@-](?:\\d{4,}|[a-z][\\w-]*))?(?:-v\\d+:\\d+)?$': {
min: 1024,
max: 64_000
@@ -776,6 +780,8 @@ const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
min: 1024,
max: 32_000
},
// 3.7
'(?:anthropic\\.)?claude-3[.-]7.*sonnet.*(?:-v\\d+:\\d+)?$': { min: 1024, max: 64_000 },
// Baichuan models
'baichuan-m2$': { min: 0, max: 30_000 },

View File

@@ -341,9 +341,9 @@ export const isGemini3ProModel = (model: Model | undefined | null): boolean => {
* - AWS Bedrock: anthropic.claude-opus-4-6-v1
* - GCP Vertex AI: claude-opus-4-6
* @param model - The model to check
* @returns true if the model is Claude Opus 4.6
* @returns true if the model is Claude 4.6 series model
*/
export function isOpus46Model(model: Model | undefined | null): boolean {
export function isClaude46SeriesModel(model: Model | undefined | null): boolean {
if (!model) {
return false
}
@@ -352,6 +352,6 @@ export function isOpus46Model(model: Model | undefined | null): boolean {
// - Direct API: claude-opus-4-6, claude-opus-4.6
// - AWS Bedrock: anthropic.claude-opus-4-6-v1
// - GCP Vertex AI: claude-opus-4-6
const regex = /(?:anthropic\.)?claude-opus-4[.-]6(?:[@\-:][\w\-:]+)?$/i
const regex = /(?:anthropic\.)?claude-(?:opus|sonnet)-4[.-]6(?:[@\-:][\w\-:]+)?$/i
return regex.test(modelId)
}

View File

@@ -123,7 +123,7 @@ const ThinkModelTypes = [
'perplexity',
'deepseek_hybrid',
'kimi_k2_5',
'opus46'
'claude46'
] as const
/** If the model's reasoning effort could be controlled, or its reasoning behavior could be turned on/off.