mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
feat(qwen): add Qwen 3.5 series models support (#12953)
### What this PR does Before this PR: - Qwen 3.5 series models were not supported. After this PR: - Adds support for Qwen 3.5 series models. Fixes #12947 ### Why we need it and why it was done in this way The following tradeoffs were made: - N/A The following alternatives were considered: - N/A Links to places where the discussion took place: <!-- optional: slack, other GH issue, mailinglist, ... --> ### Breaking changes - None ### Special notes for your reviewer - https://bailian.console.aliyun.com/cn-beijing/?tab=doc#/doc/?type=model&url=2840914 ### Checklist - [ ] PR: The PR description is expressive enough and will help future contributors - [ ] 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) - [ ] 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) - [ ] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [ ] 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 ```release-note Add support for Qwen 3.5 series models. ```
This commit is contained in:
@@ -58,6 +58,10 @@ describe('Qwen Model Detection', () => {
|
||||
expect(isQwenReasoningModel({ id: 'qwq-32b' } as Model)).toBe(true)
|
||||
expect(isQwenReasoningModel({ id: 'qwen-plus' } as Model)).toBe(true)
|
||||
expect(isQwenReasoningModel({ id: 'qwen3-coder' } as Model)).toBe(false)
|
||||
// Qwen 3.5 series
|
||||
expect(isQwenReasoningModel({ id: 'qwen3.5-plus' } as Model)).toBe(true)
|
||||
expect(isQwenReasoningModel({ id: 'qwen3.5-plus-2026-02-15' } as Model)).toBe(true)
|
||||
expect(isQwenReasoningModel({ id: 'qwen3.5-397b-a17b' } as Model)).toBe(true)
|
||||
})
|
||||
|
||||
test('isSupportedThinkingTokenQwenModel', () => {
|
||||
@@ -69,6 +73,12 @@ describe('Qwen Model Detection', () => {
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen-plus' } as Model)).toBe(true)
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwq-32b' } as Model)).toBe(false)
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen3-coder' } as Model)).toBe(false)
|
||||
// Qwen 3.5 series
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen3.5-plus' } as Model)).toBe(true)
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen3.5-plus-2026-02-15' } as Model)).toBe(true)
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen3.5-397b-a17b' } as Model)).toBe(true)
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen3.5-thinking' } as Model)).toBe(false)
|
||||
expect(isSupportedThinkingTokenQwenModel({ id: 'qwen3.5-instruct' } as Model)).toBe(false)
|
||||
})
|
||||
|
||||
test('isVisionModel', () => {
|
||||
|
||||
@@ -407,14 +407,20 @@ describe('Qwen & Gemini thinking coverage', () => {
|
||||
'qwen-turbo-2025-04-28',
|
||||
'qwen-flash',
|
||||
'qwen3-8b',
|
||||
'qwen3-72b'
|
||||
'qwen3-72b',
|
||||
'qwen3.5-plus',
|
||||
'qwen3.5-plus-2026-02-15',
|
||||
'qwen3.5-397b-a17b'
|
||||
])('supports thinking tokens for %s', (id) => {
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id }))).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['qwen3-thinking', 'qwen3-instruct', 'qwen3-vl-thinking'])('blocks thinking tokens for %s', (id) => {
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id }))).toBe(false)
|
||||
})
|
||||
it.each(['qwen3-thinking', 'qwen3-instruct', 'qwen3-vl-thinking', 'qwen3.5-thinking', 'qwen3.5-instruct'])(
|
||||
'blocks thinking tokens for %s',
|
||||
(id) => {
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id }))).toBe(false)
|
||||
}
|
||||
)
|
||||
|
||||
it('supports thinking tokens for qwen3-max, qwen3-max-preview and qwen3-max-2026-01-23', () => {
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id: 'qwen3-max' }))).toBe(true)
|
||||
@@ -422,6 +428,12 @@ describe('Qwen & Gemini thinking coverage', () => {
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id: 'qwen3-max-2026-01-23' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('supports thinking tokens for qwen3.5 series models', () => {
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id: 'qwen3.5-plus' }))).toBe(true)
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id: 'qwen3.5-plus-2026-02-15' }))).toBe(true)
|
||||
expect(isSupportedThinkingTokenQwenModel(createModel({ id: 'qwen3.5-397b-a17b' }))).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['qwen3-thinking', 'qwen3-vl-235b-thinking'])('always thinks for %s', (id) => {
|
||||
expect(isQwenAlwaysThinkModel(createModel({ id }))).toBe(true)
|
||||
})
|
||||
@@ -720,12 +732,17 @@ describe('getThinkModelType - Comprehensive Coverage', () => {
|
||||
// qwen3-max is now a reasoning model (equivalent to qwen3-max-2026-01-23)
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3-max' }))).toBe('qwen')
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3-max-2026-01-23' }))).toBe('qwen')
|
||||
// qwen3.5 series
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3.5-plus' }))).toBe('qwen')
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3.5-plus-2026-02-15' }))).toBe('qwen')
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3.5-397b-a17b' }))).toBe('qwen')
|
||||
})
|
||||
|
||||
it('should return default for always-thinking Qwen models (not controllable)', () => {
|
||||
// qwen3-thinking and qwen3-vl-thinking always think and don't support thinking token control
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3-thinking' }))).toBe('default')
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3-vl-235b-thinking' }))).toBe('default')
|
||||
expect(getThinkModelType(createModel({ id: 'qwen3.5-thinking' }))).toBe('default')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1369,6 +1386,10 @@ describe('findTokenLimit', () => {
|
||||
{ modelId: 'qwen3-max', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'qwen3-max-2026-01-23', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'qwen3-max-preview', expected: { min: 0, max: 81_920 } },
|
||||
// qwen3.5 series (max thinking budget: 81920)
|
||||
{ modelId: 'qwen3.5-plus', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'qwen3.5-plus-2026-02-15', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'qwen3.5-397b-a17b', expected: { min: 0, max: 81_920 } },
|
||||
{ modelId: 'Baichuan-M2', expected: { min: 0, max: 30_000 } },
|
||||
{ modelId: 'baichuan-m2', expected: { min: 0, max: 30_000 } },
|
||||
{ modelId: 'Baichuan-M3', expected: { min: 0, max: 30_000 } },
|
||||
@@ -2007,12 +2028,35 @@ describe('getModelSupportedReasoningEffortOptions', () => {
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
// qwen3.5 series
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'qwen3.5-plus' }))).toEqual([
|
||||
'default',
|
||||
'none',
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'qwen3.5-plus-2026-02-15' }))).toEqual([
|
||||
'default',
|
||||
'none',
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'qwen3.5-397b-a17b' }))).toEqual([
|
||||
'default',
|
||||
'none',
|
||||
'low',
|
||||
'medium',
|
||||
'high'
|
||||
])
|
||||
})
|
||||
|
||||
it('should return undefined for always-thinking Qwen models', () => {
|
||||
// These models always think and don't support thinking token control
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'qwen3-thinking' }))).toBeUndefined()
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'qwen3-vl-235b-thinking' }))).toBeUndefined()
|
||||
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'qwen3.5-thinking' }))).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -146,6 +146,14 @@ describe('isFunctionCallingModel', () => {
|
||||
expect(isFunctionCallingModel(createModel({ id: 'deepseek-coder', provider: 'deepseek' }))).toBe(true)
|
||||
})
|
||||
|
||||
it('supports Qwen models through qwen regex match', () => {
|
||||
expect(isFunctionCallingModel(createModel({ id: 'qwen-plus', provider: 'dashscope' }))).toBe(true)
|
||||
expect(isFunctionCallingModel(createModel({ id: 'qwen3-max', provider: 'dashscope' }))).toBe(true)
|
||||
expect(isFunctionCallingModel(createModel({ id: 'qwen3.5-plus', provider: 'dashscope' }))).toBe(true)
|
||||
expect(isFunctionCallingModel(createModel({ id: 'qwen3.5-plus-2026-02-15', provider: 'dashscope' }))).toBe(true)
|
||||
expect(isFunctionCallingModel(createModel({ id: 'qwen3.5-397b-a17b', provider: 'dashscope' }))).toBe(true)
|
||||
})
|
||||
|
||||
describe('Doubao Seed 2.0 Models', () => {
|
||||
it('should identify doubao-seed-2-0-pro-260215 as function calling model', () => {
|
||||
const model: Model = {
|
||||
|
||||
@@ -319,6 +319,16 @@ describe('isVisionModel', () => {
|
||||
expect(isVisionModel(createModel({ id: 'kimi-k2-thinking' }))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Qwen Models', () => {
|
||||
it('should return true for Qwen vision models', () => {
|
||||
expect(isVisionModel(createModel({ id: 'qwen-vl-max' }))).toBe(true)
|
||||
expect(isVisionModel(createModel({ id: 'qwen3-vl' }))).toBe(true)
|
||||
expect(isVisionModel(createModel({ id: 'qwen3.5-plus' }))).toBe(true)
|
||||
expect(isVisionModel(createModel({ id: 'qwen3.5-plus-2026-02-15' }))).toBe(true)
|
||||
expect(isVisionModel(createModel({ id: 'qwen3.5-397b-a17b' }))).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Doubao Seed 2.0 Models', () => {
|
||||
|
||||
@@ -848,6 +848,14 @@ export const SYSTEM_MODELS: Record<SystemProviderId | 'defaultModel', Model[]> =
|
||||
{ id: 'qwen-plus', name: 'qwen-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
|
||||
{ id: 'qwen-max', name: 'qwen-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' },
|
||||
{ id: 'qwen3-max', name: 'qwen3-max', provider: 'dashscope', group: 'qwen-max', owned_by: 'system' },
|
||||
{ id: 'qwen3.5-plus', name: 'qwen3.5-plus', provider: 'dashscope', group: 'qwen-plus', owned_by: 'system' },
|
||||
{
|
||||
id: 'qwen3.5-397b-a17b',
|
||||
name: 'qwen3.5-397b-a17b',
|
||||
provider: 'dashscope',
|
||||
group: 'qwen-plus',
|
||||
owned_by: 'system'
|
||||
},
|
||||
{ id: 'text-embedding-v4', name: 'text-embedding-v4', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
{ id: 'text-embedding-v3', name: 'text-embedding-v3', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
{ id: 'text-embedding-v2', name: 'text-embedding-v2', provider: 'dashscope', group: 'qwen-text-embedding' },
|
||||
|
||||
@@ -448,7 +448,10 @@ export function isSupportedThinkingTokenQwenModel(model?: Model): boolean {
|
||||
'qwen-flash-2025-07-28',
|
||||
'qwen3-max', // qwen3-max is now a reasoning model (equivalent to qwen3-max-2026-01-23)
|
||||
'qwen3-max-2026-01-23',
|
||||
'qwen3-max-preview'
|
||||
'qwen3-max-preview',
|
||||
'qwen3.5-plus',
|
||||
'qwen3.5-plus-2026-02-15',
|
||||
'qwen3.5-397b-a17b'
|
||||
].includes(modelId)
|
||||
}
|
||||
|
||||
@@ -754,6 +757,9 @@ const THINKING_TOKEN_MAP: Record<string, { min: number; max: number }> = {
|
||||
'qwen-flash.*$': { min: 0, max: 81_920 },
|
||||
// qwen3-max series (reasoning models, equivalent to qwen-plus for thinking budget)
|
||||
'qwen3-max(-.*)?$': { min: 0, max: 81_920 },
|
||||
// Qwen3.5 series (max thinking budget: 81920)
|
||||
'qwen3\\.5-plus.*$': { min: 0, max: 81_920 },
|
||||
'qwen3\\.5-397b-a17b$': { min: 0, max: 81_920 },
|
||||
'qwen3-(?!max).*$': { min: 1024, max: 38_912 },
|
||||
|
||||
// Claude models (supports AWS Bedrock 'anthropic.' prefix, GCP Vertex AI '@' separator, and '-v1:0' suffix)
|
||||
|
||||
@@ -26,6 +26,7 @@ const visionAllowedModels = [
|
||||
'qwen2-vl',
|
||||
'qwen2.5-vl',
|
||||
'qwen3-vl',
|
||||
'qwen3\\.5(?:-[\\w-]+)?',
|
||||
'qwen2.5-omni',
|
||||
'qwen3-omni(?:-[\\w-]+)?',
|
||||
'qvq',
|
||||
|
||||
Reference in New Issue
Block a user