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:
George·Dong
2026-02-18 10:52:13 +08:00
committed by GitHub
parent 8fd07180e2
commit cc3d440245
7 changed files with 92 additions and 5 deletions

View File

@@ -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', () => {

View File

@@ -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()
})
})

View File

@@ -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 = {

View File

@@ -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', () => {

View File

@@ -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' },

View File

@@ -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)

View File

@@ -26,6 +26,7 @@ const visionAllowedModels = [
'qwen2-vl',
'qwen2.5-vl',
'qwen3-vl',
'qwen3\\.5(?:-[\\w-]+)?',
'qwen2.5-omni',
'qwen3-omni(?:-[\\w-]+)?',
'qvq',