diff --git a/package.json b/package.json index 83423e792b..38301e8405 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/patches/@ai-sdk__anthropic.patch b/patches/@ai-sdk__anthropic.patch new file mode 100644 index 0000000000..8f5d609e3b --- /dev/null +++ b/patches/@ai-sdk__anthropic.patch @@ -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) && { diff --git a/patches/@ai-sdk__deepseek@2.0.29.patch b/patches/@ai-sdk__deepseek@2.0.29.patch new file mode 100644 index 0000000000..143450c608 --- /dev/null +++ b/patches/@ai-sdk__deepseek@2.0.29.patch @@ -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] + }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2d7bb6d45..beda756f7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 diff --git a/src/renderer/src/aiCore/utils/__tests__/reasoning.test.ts b/src/renderer/src/aiCore/utils/__tests__/reasoning.test.ts index 7c2b851622..c608ee6c57 100644 --- a/src/renderer/src/aiCore/utils/__tests__/reasoning.test.ts +++ b/src/renderer/src/aiCore/utils/__tests__/reasoning.test.ts @@ -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', () => { diff --git a/src/renderer/src/aiCore/utils/reasoning.ts b/src/renderer/src/aiCore/utils/reasoning.ts index 62b145a0d2..f021955e05 100644 --- a/src/renderer/src/aiCore/utils/reasoning.ts +++ b/src/renderer/src/aiCore/utils/reasoning.ts @@ -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 } { +): { + thinking?: AnthropicProviderOptions['thinking'] + effort?: Exclude + 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> = { + 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 } } diff --git a/src/renderer/src/config/models/__tests__/reasoning.test.ts b/src/renderer/src/config/models/__tests__/reasoning.test.ts index d0c765f964..0a0b1ea6e0 100644 --- a/src/renderer/src/config/models/__tests__/reasoning.test.ts +++ b/src/renderer/src/config/models/__tests__/reasoning.test.ts @@ -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) })