mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-06 05:55:28 +08:00
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:
@@ -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",
|
||||
|
||||
28
patches/@ai-sdk__anthropic.patch
Normal file
28
patches/@ai-sdk__anthropic.patch
Normal 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) && {
|
||||
54
patches/@ai-sdk__deepseek@2.0.29.patch
Normal file
54
patches/@ai-sdk__deepseek@2.0.29.patch
Normal 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
24
pnpm-lock.yaml
generated
@@ -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
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user