hotfix(deepseek): forward reasoning effort for DeepSeek V4+ via Claude endpoint (#14572)

### What this PR does

Before this PR:

- For DeepSeek V4+ models served through a Claude-compatible endpoint,
`getAnthropicReasoningParams` returned only `{ thinking: { type:
'enabled', budgetTokens } }`. The requested `reasoning_effort` was
dropped before the request left the client, so `high` / `xhigh` behaved
identically to default.
- `@ai-sdk/deepseek` had no `reasoning_effort` field on its Zod options
schema, so even if the effort were forwarded the SDK would strip it from
the request body.
- `@ai-sdk/anthropic` silently omitted the `thinking` field when
thinking was turned off, preventing downstream providers that require an
explicit `{ type: 'disabled' }` from honoring the disable.

After this PR:

- Non-Anthropic Claude-endpoint models (Kimi, MiniMax, DeepSeek V4+,
etc.) receive `sendReasoning: true` so reasoning output is actually
streamed back.
- DeepSeek V4+ additionally receives `effort: 'max'` when the user picks
`xhigh`, otherwise `effort: 'high'`.
- `@ai-sdk/deepseek@2.0.29` is patched to accept `reasoning_effort` on
its language-model options schema and forward it into the request body.
- `@ai-sdk/anthropic` is patched to emit an explicit `thinking: { type:
'disabled' }` when thinking is disabled.
- Updated one existing `getAnthropicReasoningParams` unit test to assert
the new `sendReasoning: true` payload, and added a new V4+ reasoning
test suite.

Fixes #

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

Follow-up to #14551, which introduced `isDeepSeekV4PlusModel` and the
`deepseek_v4` reasoning category but did not wire the selected effort
into the outgoing request for the Claude-compatible endpoint path.
Without this PR the `high` / `xhigh` options are cosmetic for V4+.

The following tradeoffs were made:

- Patched upstream SDKs (`@ai-sdk/deepseek`, `@ai-sdk/anthropic`)
instead of waiting for upstream releases, so the fix ships on `main`'s
code freeze window. Patches are narrow (single field additions).
- Kept all non-Anthropic Claude-endpoint models on `sendReasoning: true`
rather than gating per-provider — the field is harmless for providers
that don't emit reasoning and restores visibility for those that do.

The following alternatives were considered:

- Upstreaming the `reasoning_effort` change to `@ai-sdk/deepseek` —
rejected for timing; patch is mechanical and easy to drop once upstream
lands.
- Sending effort via a custom header — rejected; DeepSeek documents
`reasoning_effort` in the request body.

Links to places where the discussion took place:

### Breaking changes

None. The added fields (`sendReasoning`, `effort`, `reasoning_effort`)
are additive and only take effect for models already routed through the
Claude-compatible path with reasoning enabled.

### Special notes for your reviewer

- Two new patch files under `patches/` are registered in
`package.json#pnpm.patchedDependencies`; they are narrow dist-only
additions.
- The implementation change is localized to
`getAnthropicReasoningParams` in
`src/renderer/src/aiCore/utils/reasoning.ts`.
- Targeting `main` via `hotfix/*` branch per the branch-strategy rule,
since this restores intended V4+ behavior shipped in #14551 and contains
no refactoring.

### Checklist

- [x] PR: The PR description is expressive enough and will help future
contributors
- [x] Code: Write code that humans can understand and Keep it simple
- [x] Refactor: You have left the code cleaner than you found it (Boy
Scout Rule)
- [x] Upgrade: Impact of this change on upgrade flows was considered and
addressed if required
- [x] Documentation: A user-guide update was considered and is present
(link) or not required. Check this only when the PR introduces or
changes a user-facing feature or behavior.
- [x] Self-review: I have reviewed my own code before requesting review
from others

### Release note

```release-note
Fix: DeepSeek V4+ reasoning effort (high / xhigh) is now actually forwarded to the API when the model is served via a Claude-compatible endpoint; reasoning output is also streamed back to the UI.
```

---------

Signed-off-by: suyao <sy20010504@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
SuYao
2026-04-25 02:12:47 +08:00
committed by GitHub
parent 5a1b20fe08
commit 4e1e4548bb
7 changed files with 372 additions and 13 deletions

View File

@@ -498,7 +498,9 @@
"@opeoginni/github-copilot-openai-compatible@1.0.0": "patches/@opeoginni__github-copilot-openai-compatible@1.0.0.patch",
"@ai-sdk/openai-compatible@2.0.37": "patches/@ai-sdk__openai-compatible@2.0.37.patch",
"@ai-sdk/openai@3.0.53": "patches/@ai-sdk__openai@3.0.53.patch",
"@ai-sdk/google@3.0.64": "patches/@ai-sdk__google@3.0.64.patch"
"@ai-sdk/google@3.0.64": "patches/@ai-sdk__google@3.0.64.patch",
"@ai-sdk/anthropic": "patches/@ai-sdk__anthropic.patch",
"@ai-sdk/deepseek@2.0.29": "patches/@ai-sdk__deepseek@2.0.29.patch"
},
"onlyBuiltDependencies": [
"@j178/prek",

View File

@@ -0,0 +1,28 @@
diff --git a/dist/internal/index.js b/dist/internal/index.js
index 468e7518923b5bfaffa3a5a601e233b5271889ae..88d6a8c15902c65a4eed312eba974fb1f3026a72 100644
--- a/dist/internal/index.js
+++ b/dist/internal/index.js
@@ -3062,6 +3062,9 @@ var AnthropicMessagesLanguageModel = class {
...thinkingDisplay != null && { display: thinkingDisplay }
}
},
+ ...thinkingType === "disabled" && {
+ thinking: { type: "disabled" }
+ },
...((anthropicOptions == null ? void 0 : anthropicOptions.effort) || (anthropicOptions == null ? void 0 : anthropicOptions.taskBudget) || useStructuredOutput && (responseFormat == null ? void 0 : responseFormat.type) === "json" && responseFormat.schema != null) && {
output_config: {
...(anthropicOptions == null ? void 0 : anthropicOptions.effort) && {
diff --git a/dist/internal/index.mjs b/dist/internal/index.mjs
index 6e9ff51dd8ccc4785ecd423ec34a84eba8a7d42d..0bde5ccd9dc588b5aec4668b06d7590cfb18c768 100644
--- a/dist/internal/index.mjs
+++ b/dist/internal/index.mjs
@@ -3091,6 +3091,9 @@ var AnthropicMessagesLanguageModel = class {
...thinkingDisplay != null && { display: thinkingDisplay }
}
},
+ ...thinkingType === "disabled" && {
+ thinking: { type: "disabled" }
+ },
...((anthropicOptions == null ? void 0 : anthropicOptions.effort) || (anthropicOptions == null ? void 0 : anthropicOptions.taskBudget) || useStructuredOutput && (responseFormat == null ? void 0 : responseFormat.type) === "json" && responseFormat.schema != null) && {
output_config: {
...(anthropicOptions == null ? void 0 : anthropicOptions.effort) && {

View File

@@ -0,0 +1,54 @@
diff --git a/dist/index.js b/dist/index.js
index 243aa9c186c2b647bfd307984e3a8ad8d2f00086..14406ffff6acb8a3d38eb338d20da069eee1875a 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -303,7 +303,11 @@ var deepseekLanguageModelOptions = import_v42.z.object({
*/
thinking: import_v42.z.object({
type: import_v42.z.enum(["enabled", "disabled"]).optional()
- }).optional()
+ }).optional(),
+ /**
+ * Reasoning effort for DeepSeek V4+ models. Accepts `"high"` or `"max"`.
+ */
+ reasoning_effort: import_v42.z.enum(["high", "max"]).optional()
});
// src/chat/deepseek-prepare-tools.ts
@@ -470,7 +474,8 @@ var DeepSeekChatLanguageModel = class {
messages,
tools: deepseekTools,
tool_choice: deepseekToolChoices,
- thinking: ((_b = deepseekOptions.thinking) == null ? void 0 : _b.type) != null ? { type: deepseekOptions.thinking.type } : void 0
+ thinking: ((_b = deepseekOptions.thinking) == null ? void 0 : _b.type) != null ? { type: deepseekOptions.thinking.type } : void 0,
+ reasoning_effort: deepseekOptions.reasoning_effort
},
warnings: [...warnings, ...toolWarnings]
};
diff --git a/dist/index.mjs b/dist/index.mjs
index 6a2d2d14739e3b50faa398ff94092bd5b0ea874d..947eb026b4eecd256c1009be364b35aaa3c2e628 100644
--- a/dist/index.mjs
+++ b/dist/index.mjs
@@ -292,7 +292,11 @@ var deepseekLanguageModelOptions = z2.object({
*/
thinking: z2.object({
type: z2.enum(["enabled", "disabled"]).optional()
- }).optional()
+ }).optional(),
+ /**
+ * Reasoning effort for DeepSeek V4+ models. Accepts `"high"` or `"max"`.
+ */
+ reasoning_effort: z2.enum(["high", "max"]).optional()
});
// src/chat/deepseek-prepare-tools.ts
@@ -459,7 +463,8 @@ var DeepSeekChatLanguageModel = class {
messages,
tools: deepseekTools,
tool_choice: deepseekToolChoices,
- thinking: ((_b = deepseekOptions.thinking) == null ? void 0 : _b.type) != null ? { type: deepseekOptions.thinking.type } : void 0
+ thinking: ((_b = deepseekOptions.thinking) == null ? void 0 : _b.type) != null ? { type: deepseekOptions.thinking.type } : void 0,
+ reasoning_effort: deepseekOptions.reasoning_effort
},
warnings: [...warnings, ...toolWarnings]
};

24
pnpm-lock.yaml generated
View File

@@ -34,6 +34,12 @@ overrides:
'@ai-sdk/provider-utils': 4.0.23
patchedDependencies:
'@ai-sdk/anthropic':
hash: 306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58
path: patches/@ai-sdk__anthropic.patch
'@ai-sdk/deepseek@2.0.29':
hash: db4c4c1e60c18e61952ec4432de7a401adf44420bdbfe4bb6f726a8958642790
path: patches/@ai-sdk__deepseek@2.0.29.patch
'@ai-sdk/google@3.0.64':
hash: 3dbdd2fbbe5a44bc23a04b1195329dd8d78043447e925342ad5ed53c8c3b96ec
path: patches/@ai-sdk__google@3.0.64.patch
@@ -177,7 +183,7 @@ importers:
version: 4.0.96(zod@4.3.4)
'@ai-sdk/anthropic':
specifier: ^3.0.71
version: 3.0.71(zod@4.3.4)
version: 3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.4)
'@ai-sdk/azure':
specifier: ^3.0.54
version: 3.0.54(zod@4.3.4)
@@ -1291,7 +1297,7 @@ importers:
dependencies:
'@ai-sdk/anthropic':
specifier: ^3.0.71
version: 3.0.71(zod@4.3.6)
version: 3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.6)
'@ai-sdk/google':
specifier: ^3.0.64
version: 3.0.64(patch_hash=3dbdd2fbbe5a44bc23a04b1195329dd8d78043447e925342ad5ed53c8c3b96ec)(zod@4.3.6)
@@ -1322,13 +1328,13 @@ importers:
dependencies:
'@ai-sdk/anthropic':
specifier: ^3.0.71
version: 3.0.71(zod@4.3.4)
version: 3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.4)
'@ai-sdk/azure':
specifier: ^3.0.54
version: 3.0.54(zod@4.3.4)
'@ai-sdk/deepseek':
specifier: ^2.0.29
version: 2.0.29(zod@4.3.4)
version: 2.0.29(patch_hash=db4c4c1e60c18e61952ec4432de7a401adf44420bdbfe4bb6f726a8958642790)(zod@4.3.4)
'@ai-sdk/google':
specifier: ^3.0.64
version: 3.0.64(patch_hash=3dbdd2fbbe5a44bc23a04b1195329dd8d78043447e925342ad5ed53c8c3b96ec)(zod@4.3.4)
@@ -12927,7 +12933,7 @@ snapshots:
'@ai-sdk/amazon-bedrock@4.0.96(zod@4.3.4)':
dependencies:
'@ai-sdk/anthropic': 3.0.71(zod@4.3.4)
'@ai-sdk/anthropic': 3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.4)
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.4)
'@smithy/eventstream-codec': 4.2.14
@@ -12935,13 +12941,13 @@ snapshots:
aws4fetch: 1.0.20
zod: 4.3.4
'@ai-sdk/anthropic@3.0.71(zod@4.3.4)':
'@ai-sdk/anthropic@3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.4)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/anthropic@3.0.71(zod@4.3.6)':
'@ai-sdk/anthropic@3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.6)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.6)
@@ -12967,7 +12973,7 @@ snapshots:
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.4)
zod: 4.3.4
'@ai-sdk/deepseek@2.0.29(zod@4.3.4)':
'@ai-sdk/deepseek@2.0.29(patch_hash=db4c4c1e60c18e61952ec4432de7a401adf44420bdbfe4bb6f726a8958642790)(zod@4.3.4)':
dependencies:
'@ai-sdk/provider': 3.0.8
'@ai-sdk/provider-utils': 4.0.23(zod@4.3.4)
@@ -12989,7 +12995,7 @@ snapshots:
'@ai-sdk/google-vertex@4.0.112(zod@4.3.4)':
dependencies:
'@ai-sdk/anthropic': 3.0.71(zod@4.3.4)
'@ai-sdk/anthropic': 3.0.71(patch_hash=306ee28d1836838d4f6bb8b2f3541756846d36f7b3d85c594d62d0301b91ad58)(zod@4.3.4)
'@ai-sdk/google': 3.0.64(patch_hash=3dbdd2fbbe5a44bc23a04b1195329dd8d78043447e925342ad5ed53c8c3b96ec)(zod@4.3.4)
'@ai-sdk/openai-compatible': 2.0.41(zod@4.3.4)
'@ai-sdk/provider': 3.0.8

View File

@@ -81,6 +81,7 @@ vi.mock('@renderer/config/models', async (importOriginal) => {
isSupportedThinkingTokenMiMoModel: vi.fn(() => false),
isSupportedReasoningEffortModel: vi.fn(() => false),
isDeepSeekHybridInferenceModel: vi.fn(() => false),
isDeepSeekV4PlusModel: vi.fn(() => false),
isSupportedReasoningEffortGrokModel: vi.fn(() => false),
getThinkModelType: vi.fn(() => 'default'),
isDoubaoSeedAfter251015: vi.fn(() => false),
@@ -828,7 +829,8 @@ describe('reasoning utils', () => {
thinking: {
type: 'enabled',
budgetTokens: expect.any(Number)
}
},
sendReasoning: true
})
const thinking = result.thinking as { type: 'enabled'; budgetTokens?: number }
expect(thinking.budgetTokens).toBeGreaterThanOrEqual(1024)
@@ -869,6 +871,125 @@ describe('reasoning utils', () => {
const highThinking = highResult.thinking as { type: 'enabled'; budgetTokens?: number }
expect(highThinking.budgetTokens).toBeGreaterThanOrEqual(lowThinking.budgetTokens!)
})
it('should map DeepSeek V4+ xhigh effort to max on the Claude endpoint', async () => {
const { isReasoningModel, isSupportedThinkingTokenClaudeModel, isDeepSeekV4PlusModel, findTokenLimit } =
await import('@renderer/config/models')
vi.mocked(isReasoningModel).mockReturnValue(true)
vi.mocked(isSupportedThinkingTokenClaudeModel).mockReturnValue(false)
vi.mocked(isDeepSeekV4PlusModel).mockReturnValue(true)
vi.mocked(findTokenLimit).mockReturnValue(undefined)
const model: Model = {
id: 'deepseek-v4-pro',
name: 'DeepSeek V4 Pro',
provider: 'deepseek'
} as Model
const assistant: Assistant = {
id: 'test',
name: 'Test',
settings: { reasoning_effort: 'xhigh', maxTokens: 4096 }
} as Assistant
const result = getAnthropicReasoningParams(assistant, model)
expect(result).toEqual({
thinking: { type: 'enabled', budgetTokens: expect.any(Number) },
sendReasoning: true,
effort: 'max'
})
})
it('should map DeepSeek V4+ high effort to high on the Claude endpoint', async () => {
const { isReasoningModel, isSupportedThinkingTokenClaudeModel, isDeepSeekV4PlusModel, findTokenLimit } =
await import('@renderer/config/models')
vi.mocked(isReasoningModel).mockReturnValue(true)
vi.mocked(isSupportedThinkingTokenClaudeModel).mockReturnValue(false)
vi.mocked(isDeepSeekV4PlusModel).mockReturnValue(true)
vi.mocked(findTokenLimit).mockReturnValue(undefined)
const model: Model = {
id: 'deepseek-v4',
name: 'DeepSeek V4',
provider: 'deepseek'
} as Model
const assistant: Assistant = {
id: 'test',
name: 'Test',
settings: { reasoning_effort: 'high', maxTokens: 4096 }
} as Assistant
const result = getAnthropicReasoningParams(assistant, model)
expect(result).toEqual({
thinking: { type: 'enabled', budgetTokens: expect.any(Number) },
sendReasoning: true,
effort: 'high'
})
})
it('should not add effort for DeepSeek V4+ when effort is outside the documented set', async () => {
// Guard against silent downgrade: if MODEL_SUPPORTED_REASONING_EFFORT.deepseek_v4 ever gains
// new levels (low/medium/auto), the explicit effortMap must be extended — otherwise effort
// is omitted here rather than being silently mapped to 'high'.
const { isReasoningModel, isSupportedThinkingTokenClaudeModel, isDeepSeekV4PlusModel, findTokenLimit } =
await import('@renderer/config/models')
vi.mocked(isReasoningModel).mockReturnValue(true)
vi.mocked(isSupportedThinkingTokenClaudeModel).mockReturnValue(false)
vi.mocked(isDeepSeekV4PlusModel).mockReturnValue(true)
vi.mocked(findTokenLimit).mockReturnValue(undefined)
const model: Model = {
id: 'deepseek-v4',
name: 'DeepSeek V4',
provider: 'deepseek'
} as Model
const assistant: Assistant = {
id: 'test',
name: 'Test',
settings: { reasoning_effort: 'medium', maxTokens: 4096 }
} as Assistant
const result = getAnthropicReasoningParams(assistant, model)
expect(result).toEqual({
thinking: { type: 'enabled', budgetTokens: expect.any(Number) },
sendReasoning: true
})
expect(result).not.toHaveProperty('effort')
})
it('should not add effort for non-DeepSeek models on the Claude endpoint', async () => {
const { isReasoningModel, isSupportedThinkingTokenClaudeModel, isDeepSeekV4PlusModel, findTokenLimit } =
await import('@renderer/config/models')
vi.mocked(isReasoningModel).mockReturnValue(true)
vi.mocked(isSupportedThinkingTokenClaudeModel).mockReturnValue(false)
vi.mocked(isDeepSeekV4PlusModel).mockReturnValue(false)
vi.mocked(findTokenLimit).mockReturnValue(undefined)
const model: Model = {
id: 'kimi-k2-reasoning',
name: 'Kimi K2 Reasoning',
provider: 'custom-provider'
} as Model
const assistant: Assistant = {
id: 'test',
name: 'Test',
settings: { reasoning_effort: 'xhigh', maxTokens: 4096 }
} as Assistant
const result = getAnthropicReasoningParams(assistant, model)
expect(result).toEqual({
thinking: { type: 'enabled', budgetTokens: expect.any(Number) },
sendReasoning: true
})
expect(result).not.toHaveProperty('effort')
})
})
describe('getGeminiReasoningParams', () => {

View File

@@ -681,11 +681,19 @@ function getFallbackBudgetTokens(reasoningEffort: string | undefined): number {
* 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.
* - **Non-Anthropic models served via the Claude-compatible endpoint** (Kimi, MiniMax,
* DeepSeek V4+, etc.): `{ thinking: { type: 'enabled', budgetTokens: number }, sendReasoning: true, effort? }`
* `sendReasoning: true` ensures reasoning output is streamed back to the UI.
* `effort` is only added for DeepSeek V4+ (`high` | `xhigh` → `high` | `max`).
*/
export function getAnthropicReasoningParams(
assistant: Assistant,
model: Model
): { thinking?: AnthropicProviderOptions['thinking']; effort?: Exclude<AnthropicProviderOptions['effort'], 'xhigh'> } {
): {
thinking?: AnthropicProviderOptions['thinking']
effort?: Exclude<AnthropicProviderOptions['effort'], 'xhigh'>
sendReasoning?: AnthropicProviderOptions['sendReasoning']
} {
if (!isReasoningModel(model)) {
return {}
}
@@ -742,10 +750,33 @@ export function getAnthropicReasoningParams(
// 其他使用claude端點的模型比如Kimi,Minimax等等
const { maxTokens } = getAssistantSettings(assistant)
const budgetTokens = getThinkingBudget(maxTokens, reasoningEffort, model.id)
const params: Partial<ReturnType<typeof getAnthropicReasoningParams>> = {
thinking: {
type: 'enabled',
budgetTokens: budgetTokens ?? getFallbackBudgetTokens(reasoningEffort)
},
sendReasoning: true
}
// https://api-docs.deepseek.com/guides/thinking_mode
// DeepSeek V4+ exposes only 'high' and 'xhigh' as user-facing effort levels
// (see MODEL_SUPPORTED_REASONING_EFFORT.deepseek_v4); default/none are already
// short-circuited earlier in this function. The explicit map avoids silently
// downgrading future levels (low/medium/auto) to 'high' — unmapped values are
// simply omitted so callers fall back to API defaults instead.
if (isDeepSeekV4PlusModel(model)) {
const deepSeekV4EffortMap = {
high: 'high',
xhigh: 'max'
} as const
const effort = deepSeekV4EffortMap[reasoningEffort as keyof typeof deepSeekV4EffortMap]
if (effort) {
params.effort = effort
}
}
// Always include budgetTokens to prevent Claude Agent SDK from converting
// { type: 'enabled' } into '--thinking adaptive', which non-Anthropic
// upstream providers do not support (they only accept 'enabled'/'disabled').
return { thinking: { type: 'enabled', budgetTokens: budgetTokens ?? getFallbackBudgetTokens(reasoningEffort) } }
return params
}
}

View File

@@ -11,6 +11,7 @@ import {
isClaude45ReasoningModel,
isClaudeReasoningModel,
isDeepSeekHybridInferenceModel,
isDeepSeekV4PlusModel,
isDoubaoSeedAfter251015,
isDoubaoThinkingAutoModel,
isFixedReasoningModel,
@@ -401,6 +402,117 @@ describe('DeepSeek & Thinking Tokens', () => {
})
})
describe('DeepSeek V4+ Models', () => {
describe('isDeepSeekV4PlusModel', () => {
it('matches V4 model IDs with and without suffixes', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v4' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v4-flash' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v4-pro' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v4.1' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v4-pro-preview' }))).toBe(true)
})
it('matches future V5+ and double-digit versions via wildcard regex', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v5' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v5-flash' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v9-pro' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v10' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v42-ultra' }))).toBe(true)
})
it('matches prefixed model IDs from aggregators and agent routes', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'custom-deepseek-v4' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'prefix-deepseek-v4-flash' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'agent/deepseek-v4-pro' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'accounts/fireworks/models/deepseek-v4-pro' }))).toBe(true)
})
it('is case insensitive', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'DeepSeek-V4' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'DEEPSEEK-V4-FLASH' }))).toBe(true)
})
it('falls back to model name when id does not match', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'custom-id', name: 'deepseek-v4-pro' }))).toBe(true)
expect(isDeepSeekV4PlusModel(createModel({ id: 'custom-id', name: 'DeepSeek-V5' }))).toBe(true)
})
it('rejects V3 and older versions', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v3' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v3.1' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v3.2' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v2' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-v1' }))).toBe(false)
})
it('rejects unrelated model IDs', () => {
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-chat' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'deepseek-reasoner' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'gpt-4' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: 'claude-v4' }))).toBe(false)
expect(isDeepSeekV4PlusModel(createModel({ id: '' }))).toBe(false)
})
})
describe('isDeepSeekHybridInferenceModel integration', () => {
it('includes V4+ models via delegation to isDeepSeekV4PlusModel', () => {
expect(isDeepSeekHybridInferenceModel(createModel({ id: 'deepseek-v4' }))).toBe(true)
expect(isDeepSeekHybridInferenceModel(createModel({ id: 'deepseek-v4-flash' }))).toBe(true)
expect(isDeepSeekHybridInferenceModel(createModel({ id: 'deepseek-v4-pro' }))).toBe(true)
expect(isDeepSeekHybridInferenceModel(createModel({ id: 'deepseek-v5-xxx' }))).toBe(true)
expect(isDeepSeekHybridInferenceModel(createModel({ id: 'accounts/fireworks/models/deepseek-v4-pro' }))).toBe(
true
)
})
})
describe('getThinkModelType', () => {
it('returns deepseek_v4 for V4+ models', () => {
expect(getThinkModelType(createModel({ id: 'deepseek-v4' }))).toBe('deepseek_v4')
expect(getThinkModelType(createModel({ id: 'deepseek-v4-flash' }))).toBe('deepseek_v4')
expect(getThinkModelType(createModel({ id: 'deepseek-v4-pro' }))).toBe('deepseek_v4')
expect(getThinkModelType(createModel({ id: 'deepseek-v5' }))).toBe('deepseek_v4')
expect(getThinkModelType(createModel({ id: 'deepseek-v10-ultra' }))).toBe('deepseek_v4')
})
it('prioritizes deepseek_v4 over deepseek_hybrid', () => {
// V4+ is checked before hybrid; make sure V4 never falls through to hybrid classification.
expect(getThinkModelType(createModel({ id: 'deepseek-v4-pro' }))).toBe('deepseek_v4')
expect(getThinkModelType(createModel({ id: 'deepseek-v3.1' }))).toBe('deepseek_hybrid')
})
it('is case insensitive', () => {
expect(getThinkModelType(createModel({ id: 'DeepSeek-V4' }))).toBe('deepseek_v4')
expect(getThinkModelType(createModel({ id: 'DEEPSEEK-V4-PRO' }))).toBe('deepseek_v4')
})
})
describe('reasoning effort configuration', () => {
it('exposes high and xhigh as the only effort levels', () => {
expect(MODEL_SUPPORTED_REASONING_EFFORT.deepseek_v4).toEqual(['high', 'xhigh'])
})
it('exposes default, none, high, xhigh as user-facing options', () => {
expect(MODEL_SUPPORTED_OPTIONS.deepseek_v4).toEqual(['default', 'none', 'high', 'xhigh'])
})
it('returns correct options from getModelSupportedReasoningEffortOptions for V4+ models', () => {
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'deepseek-v4', provider: 'deepseek' }))).toEqual(
['default', 'none', 'high', 'xhigh']
)
expect(
getModelSupportedReasoningEffortOptions(createModel({ id: 'deepseek-v4-flash', provider: 'deepseek' }))
).toEqual(['default', 'none', 'high', 'xhigh'])
expect(
getModelSupportedReasoningEffortOptions(createModel({ id: 'deepseek-v4-pro', provider: 'openrouter' }))
).toEqual(['default', 'none', 'high', 'xhigh'])
expect(getModelSupportedReasoningEffortOptions(createModel({ id: 'deepseek-v5', provider: 'deepseek' }))).toEqual(
['default', 'none', 'high', 'xhigh']
)
})
})
})
describe('Qwen & Gemini thinking coverage', () => {
it.each([
'qwen-plus',
@@ -2725,6 +2837,11 @@ describe('Fireworks provider model name normalization', () => {
expect(isDeepSeekHybridInferenceModel(createModel({ id: 'accounts/fireworks/models/deepseek-v3p1' }))).toBe(true)
})
it('should classify DeepSeek V4+ models from Fireworks as deepseek_v4', () => {
expect(getThinkModelType(createModel({ id: 'accounts/fireworks/models/deepseek-v4-pro' }))).toBe('deepseek_v4')
expect(isDeepSeekV4PlusModel(createModel({ id: 'accounts/fireworks/models/deepseek-v4-flash' }))).toBe(true)
})
it('should detect Kimi reasoning models from Fireworks', () => {
expect(isKimiReasoningModel(createModel({ id: 'accounts/fireworks/models/kimi-k2p5' }))).toBe(true)
})