diff --git a/.env.example b/.env.example index 1684e1bf..42555f2b 100644 --- a/.env.example +++ b/.env.example @@ -145,6 +145,11 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here # CLAUDE_CODE_USE_OPENAI=1 # OPENAI_API_KEY=sk-your-key-here # OPENAI_MODEL=gpt-4o +# For DeepSeek, set: +# OPENAI_BASE_URL=https://api.deepseek.com/v1 +# OPENAI_MODEL=deepseek-v4-flash +# Optional: OPENAI_MODEL=deepseek-v4-pro +# Legacy aliases also work: deepseek-chat and deepseek-reasoner # Use a custom OpenAI-compatible endpoint (optional — defaults to api.openai.com) # OPENAI_BASE_URL=https://api.openai.com/v1 diff --git a/README.md b/README.md index 26e32bbf..2b9d64d2 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ Add to `~/.claude/settings.json`: ```json { "agentModels": { - "deepseek-chat": { + "deepseek-v4-flash": { "base_url": "https://api.deepseek.com/v1", "api_key": "sk-your-key" }, @@ -185,10 +185,10 @@ Add to `~/.claude/settings.json`: } }, "agentRouting": { - "Explore": "deepseek-chat", + "Explore": "deepseek-v4-flash", "Plan": "gpt-4o", "general-purpose": "gpt-4o", - "frontend-dev": "deepseek-chat", + "frontend-dev": "deepseek-v4-flash", "default": "gpt-4o" } } diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 291aee7d..5d8939dc 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -68,9 +68,11 @@ openclaude export CLAUDE_CODE_USE_OPENAI=1 export OPENAI_API_KEY=sk-... export OPENAI_BASE_URL=https://api.deepseek.com/v1 -export OPENAI_MODEL=deepseek-chat +export OPENAI_MODEL=deepseek-v4-flash ``` +Use `deepseek-v4-pro` when you want the stronger model. `deepseek-chat` and `deepseek-reasoner` remain available as DeepSeek's legacy API aliases. + ### Google Gemini via OpenRouter ```bash @@ -169,7 +171,7 @@ export OPENAI_MODEL=gpt-4o |----------|----------|-------------| | `CLAUDE_CODE_USE_OPENAI` | Yes | Set to `1` to enable the OpenAI provider | | `OPENAI_API_KEY` | Yes* | Your API key (`*` not needed for local models like Ollama or Atomic Chat) | -| `OPENAI_MODEL` | Yes | Model name such as `gpt-4o`, `deepseek-chat`, or `llama3.3:70b` | +| `OPENAI_MODEL` | Yes | Model name such as `gpt-4o`, `deepseek-v4-flash`, or `llama3.3:70b` | | `OPENAI_BASE_URL` | No | API endpoint, defaulting to `https://api.openai.com/v1` | | `CODEX_API_KEY` | Codex only | Codex or ChatGPT access token override | | `CODEX_AUTH_JSON_PATH` | Codex only | Path to a Codex CLI `auth.json` file | diff --git a/docs/quick-start-mac-linux.md b/docs/quick-start-mac-linux.md index 133c713a..63b31055 100644 --- a/docs/quick-start-mac-linux.md +++ b/docs/quick-start-mac-linux.md @@ -41,11 +41,13 @@ openclaude export CLAUDE_CODE_USE_OPENAI=1 export OPENAI_API_KEY=sk-your-key-here export OPENAI_BASE_URL=https://api.deepseek.com/v1 -export OPENAI_MODEL=deepseek-chat +export OPENAI_MODEL=deepseek-v4-flash openclaude ``` +Use `deepseek-v4-pro` when you want the stronger model. `deepseek-chat` and `deepseek-reasoner` still work as DeepSeek's legacy API aliases. + ### Option C: Ollama Install Ollama first from: diff --git a/docs/quick-start-windows.md b/docs/quick-start-windows.md index 5593fc52..43d5ca07 100644 --- a/docs/quick-start-windows.md +++ b/docs/quick-start-windows.md @@ -41,11 +41,13 @@ openclaude $env:CLAUDE_CODE_USE_OPENAI="1" $env:OPENAI_API_KEY="sk-your-key-here" $env:OPENAI_BASE_URL="https://api.deepseek.com/v1" -$env:OPENAI_MODEL="deepseek-chat" +$env:OPENAI_MODEL="deepseek-v4-flash" openclaude ``` +Use `deepseek-v4-pro` when you want the stronger model. `deepseek-chat` and `deepseek-reasoner` still work as DeepSeek's legacy API aliases. + ### Option C: Ollama Install Ollama first from: diff --git a/src/services/api/openaiShim.test.ts b/src/services/api/openaiShim.test.ts index f83622a2..96e0fa5e 100644 --- a/src/services/api/openaiShim.test.ts +++ b/src/services/api/openaiShim.test.ts @@ -3415,10 +3415,7 @@ test('Moonshot: echoes reasoning_content on assistant tool-call messages', async ) }) -test('non-Moonshot providers do NOT receive reasoning_content on assistant messages', async () => { - // Guard: only Moonshot opts in. DeepSeek/OpenRouter/etc. receive the - // outgoing assistant message without reasoning_content to avoid - // unknown-field rejections from strict servers. +test('DeepSeek echoes reasoning_content on assistant tool-call messages', async () => { process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' process.env.OPENAI_API_KEY = 'sk-deepseek' @@ -3428,7 +3425,7 @@ test('non-Moonshot providers do NOT receive reasoning_content on assistant messa return new Response( JSON.stringify({ id: 'chatcmpl-1', - model: 'deepseek-chat', + model: 'deepseek-v4-flash', choices: [ { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, ], @@ -3440,7 +3437,65 @@ test('non-Moonshot providers do NOT receive reasoning_content on assistant messa const client = createOpenAIShimClient({}) as OpenAIShimClient await client.beta.messages.create({ - model: 'deepseek-chat', + model: 'deepseek-v4-flash', + system: 'test', + messages: [ + { role: 'user', content: 'hi' }, + { + role: 'assistant', + content: [ + { type: 'thinking', thinking: 'thought' }, + { type: 'text', text: 'hello' }, + { + type: 'tool_use', + id: 'call_1', + name: 'Bash', + input: { command: 'ls' }, + }, + ], + }, + { + role: 'user', + content: [ + { type: 'tool_result', tool_use_id: 'call_1', content: 'files' }, + ], + }, + ], + max_tokens: 32, + stream: false, + }) + + const messages = requestBody?.messages as Array> + const assistantWithToolCall = messages.find( + m => m.role === 'assistant' && Array.isArray(m.tool_calls), + ) + expect(assistantWithToolCall).toBeDefined() + expect(assistantWithToolCall?.reasoning_content).toBe('thought') +}) + +test('generic OpenAI-compatible providers do not echo reasoning_content on assistant tool-call messages', async () => { + process.env.OPENAI_BASE_URL = 'https://api.openai.com/v1' + process.env.OPENAI_API_KEY = 'sk-openai-test' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'gpt-4o', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'gpt-4o', system: 'test', messages: [ { role: 'user', content: 'hi' }, @@ -3508,6 +3563,112 @@ test('Moonshot: cn host is also detected', async () => { expect(requestBody?.store).toBeUndefined() }) +test('DeepSeek sends thinking toggle and normalized reasoning effort', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-pro', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({ + reasoningEffort: 'xhigh', + }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-pro', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 64, + stream: false, + thinking: { type: 'enabled' }, + }) + + expect(requestBody?.thinking).toEqual({ type: 'enabled' }) + expect(requestBody?.reasoning_effort).toBe('max') + expect(requestBody?.max_tokens).toBe(64) + expect(requestBody?.max_completion_tokens).toBeUndefined() + expect(requestBody?.store).toBeUndefined() +}) + +test('DeepSeek omits thinking controls when the Anthropic-side request does not set them', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-flash', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-flash', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 32, + stream: false, + }) + + expect(requestBody?.thinking).toBeUndefined() + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + +test('DeepSeek forwards an explicit thinking disable toggle for V4 models', async () => { + process.env.OPENAI_BASE_URL = 'https://api.deepseek.com/v1' + process.env.OPENAI_API_KEY = 'sk-deepseek' + + let requestBody: Record | undefined + globalThis.fetch = (async (_input, init) => { + requestBody = JSON.parse(String(init?.body)) + return new Response( + JSON.stringify({ + id: 'chatcmpl-1', + model: 'deepseek-v4-flash', + choices: [ + { message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }, + ], + usage: { prompt_tokens: 3, completion_tokens: 1, total_tokens: 4 }, + }), + { headers: { 'Content-Type': 'application/json' } }, + ) + }) as FetchType + + const client = createOpenAIShimClient({}) as OpenAIShimClient + await client.beta.messages.create({ + model: 'deepseek-v4-flash', + system: 'test', + messages: [{ role: 'user', content: 'hi' }], + max_tokens: 32, + stream: false, + thinking: { type: 'disabled' }, + }) + + expect(requestBody?.thinking).toEqual({ type: 'disabled' }) + expect(requestBody?.reasoning_effort).toBeUndefined() +}) + test('collapses multiple text blocks in tool_result to string for DeepSeek compatibility (issue #774)', async () => { let requestBody: Record | undefined diff --git a/src/services/api/openaiShim.ts b/src/services/api/openaiShim.ts index 40b35c8e..6dc92223 100644 --- a/src/services/api/openaiShim.ts +++ b/src/services/api/openaiShim.ts @@ -88,6 +88,9 @@ const MOONSHOT_API_HOSTS = new Set([ 'api.moonshot.ai', 'api.moonshot.cn', ]) +const DEEPSEEK_API_HOSTS = new Set([ + 'api.deepseek.com', +]) const COPILOT_HEADERS: Record = { 'User-Agent': 'GitHubCopilotChat/0.26.7', @@ -162,6 +165,21 @@ function isMoonshotBaseUrl(baseUrl: string | undefined): boolean { } } +function isDeepSeekBaseUrl(baseUrl: string | undefined): boolean { + if (!baseUrl) return false + try { + return DEEPSEEK_API_HOSTS.has(new URL(baseUrl).hostname.toLowerCase()) + } catch { + return false + } +} + +function normalizeDeepSeekReasoningEffort( + effort: 'low' | 'medium' | 'high' | 'xhigh', +): 'high' | 'max' { + return effort === 'xhigh' ? 'max' : 'high' +} + function formatRetryAfterHint(response: Response): string { const ra = response.headers.get('retry-after') return ra ? ` (Retry-After: ${ra})` : '' @@ -1487,9 +1505,11 @@ class OpenAIShimMessages { ) const openaiMessages = convertMessages(compressedMessages, params.system, { // Moonshot requires every assistant tool-call message to carry - // reasoning_content when its thinking feature is active. Echo it back - // from the thinking block we captured on the inbound response. - preserveReasoningContent: isMoonshotBaseUrl(request.baseUrl), + // reasoning_content when its thinking feature is active. DeepSeek does + // the same for tool-call turns in thinking mode. Echo it back from the + // thinking block we captured on the inbound response. + preserveReasoningContent: + isMoonshotBaseUrl(request.baseUrl) || isDeepSeekBaseUrl(request.baseUrl), }) const body: Record = { @@ -1527,8 +1547,9 @@ class OpenAIShimMessages { const isGithubModels = isGithub && (githubEndpointType === 'models' || githubEndpointType === 'custom') const isMoonshot = isMoonshotBaseUrl(request.baseUrl) + const isDeepSeek = isDeepSeekBaseUrl(request.baseUrl) - if ((isGithub || isMistral || isLocal || isMoonshot) && body.max_completion_tokens !== undefined) { + if ((isGithub || isMistral || isLocal || isMoonshot || isDeepSeek) && body.max_completion_tokens !== undefined) { body.max_tokens = body.max_completion_tokens delete body.max_completion_tokens } @@ -1538,13 +1559,34 @@ class OpenAIShimMessages { // Moonshot (api.moonshot.ai/.cn) has not published support for the // parameter either; strip it preemptively to avoid the same class of // error on strict-parse providers. - if (isMistral || isGeminiMode() || isMoonshot) { + if (isMistral || isGeminiMode() || isMoonshot || isDeepSeek) { delete body.store } if (params.temperature !== undefined) body.temperature = params.temperature if (params.top_p !== undefined) body.top_p = params.top_p + if (isDeepSeek) { + const requestedThinkingType = (params.thinking as { type?: string } | undefined)?.type + const deepSeekThinkingType = + requestedThinkingType === 'disabled' + ? 'disabled' + : requestedThinkingType === 'enabled' || requestedThinkingType === 'adaptive' + ? 'enabled' + : undefined + + if (deepSeekThinkingType) { + body.thinking = { type: deepSeekThinkingType } + } + + if (deepSeekThinkingType === 'enabled') { + const effort = request.reasoning?.effort + if (effort) { + body.reasoning_effort = normalizeDeepSeekReasoningEffort(effort) + } + } + } + if (params.tools && params.tools.length > 0) { const converted = convertTools( params.tools as Array<{ diff --git a/src/utils/context.test.ts b/src/utils/context.test.ts index 3d6c96ec..5d8bbb06 100644 --- a/src/utils/context.test.ts +++ b/src/utils/context.test.ts @@ -31,25 +31,36 @@ afterEach(() => { } }) -test('deepseek-chat uses provider-specific context and output caps', () => { +test('deepseek-v4-flash uses provider-specific context and output caps', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS + delete process.env.OPENAI_MODEL + + expect(getContextWindowForModel('deepseek-v4-flash')).toBe(1_048_576) + expect(getModelMaxOutputTokens('deepseek-v4-flash')).toEqual({ + default: 262_144, + upperLimit: 262_144, + }) + expect(getMaxOutputTokensForModel('deepseek-v4-flash')).toBe(262_144) +}) + +test('deepseek legacy aliases keep their documented provider caps', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS delete process.env.OPENAI_MODEL expect(getContextWindowForModel('deepseek-chat')).toBe(128_000) - expect(getModelMaxOutputTokens('deepseek-chat')).toEqual({ - default: 8_192, - upperLimit: 8_192, - }) + expect(getContextWindowForModel('deepseek-reasoner')).toBe(128_000) expect(getMaxOutputTokensForModel('deepseek-chat')).toBe(8_192) + expect(getMaxOutputTokensForModel('deepseek-reasoner')).toBe(65_536) }) -test('deepseek-chat clamps oversized max output overrides to the provider limit', () => { +test('deepseek-v4-flash clamps oversized max output overrides to the provider limit', () => { process.env.CLAUDE_CODE_USE_OPENAI = '1' - process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '32000' + process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '500000' delete process.env.OPENAI_MODEL - expect(getMaxOutputTokensForModel('deepseek-chat')).toBe(8_192) + expect(getMaxOutputTokensForModel('deepseek-v4-flash')).toBe(262_144) }) test('gpt-4o uses provider-specific context and output caps', () => { diff --git a/src/utils/model/openaiContextWindows.ts b/src/utils/model/openaiContextWindows.ts index 7de2a6e7..e9d436d1 100644 --- a/src/utils/model/openaiContextWindows.ts +++ b/src/utils/model/openaiContextWindows.ts @@ -96,7 +96,13 @@ const OPENAI_CONTEXT_WINDOWS: Record = { 'o3-mini': 200_000, 'o4-mini': 200_000, - // DeepSeek (V3: 128k context per official docs) + // DeepSeek V4 coding-agent models. DeepSeek's official coding-agent guide + // publishes V4 Pro at 1,048,576 context / 262,144 output; Flash is treated + // as the same family for local budgeting until a dedicated public model card + // lands. + 'deepseek-v4-flash': 1_048_576, + 'deepseek-v4-pro': 1_048_576, + // Legacy DeepSeek API aliases documented in the public pricing/model pages. 'deepseek-chat': 128_000, 'deepseek-reasoner': 128_000, @@ -316,9 +322,12 @@ const OPENAI_MAX_OUTPUT_TOKENS: Record = { 'o3-mini': 100_000, 'o4-mini': 100_000, - // DeepSeek + // DeepSeek V4 coding-agent models. See context-window note above. + 'deepseek-v4-flash': 262_144, + 'deepseek-v4-pro': 262_144, + // Legacy DeepSeek API aliases documented in the public pricing/model pages. 'deepseek-chat': 8_192, - 'deepseek-reasoner': 32_768, + 'deepseek-reasoner': 65_536, // Groq 'llama-3.3-70b-versatile': 32_768, diff --git a/src/utils/providerModels.test.ts b/src/utils/providerModels.test.ts index f39c99c4..3a5b3b82 100644 --- a/src/utils/providerModels.test.ts +++ b/src/utils/providerModels.test.ts @@ -16,6 +16,21 @@ describe('parseModelList', () => { ]) }) + test('splits semicolon-separated models', () => { + expect(parseModelList('glm-4.7; glm-4.7-flash')).toEqual([ + 'glm-4.7', + 'glm-4.7-flash', + ]) + }) + + test('splits mixed comma- and semicolon-separated models', () => { + expect(parseModelList('gpt-5.4; gpt-5.4-mini, o3')).toEqual([ + 'gpt-5.4', + 'gpt-5.4-mini', + 'o3', + ]) + }) + test('returns single model in an array', () => { expect(parseModelList('llama3.1:8b')).toEqual(['llama3.1:8b']) }) diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index f4300726..52ea4645 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -814,6 +814,22 @@ test('openai profiles ignore poisoned shell model and base url values', () => { }) }) +test('openai profiles normalize multi-model profile values to the primary model', () => { + const env = buildOpenAIProfileEnv({ + goal: 'balanced', + apiKey: 'sk-live', + model: 'deepseek-v4-flash, deepseek-v4-pro, deepseek-chat', + baseUrl: 'https://api.deepseek.com/v1', + processEnv: {}, + }) + + assert.deepEqual(env, { + OPENAI_BASE_URL: 'https://api.deepseek.com/v1', + OPENAI_MODEL: 'deepseek-v4-flash', + OPENAI_API_KEY: 'sk-live', + }) +}) + test('startup env ignores poisoned persisted openai model and base url', async () => { const env = await buildStartupEnvFromProfile({ persisted: profile('openai', { diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index e6ec13f6..1d4ebcf7 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -15,6 +15,7 @@ import { } from './providerRecommendation.js' import { readGeminiAccessToken } from './geminiCredentials.js' import { getOllamaChatBaseUrl } from './providerDiscovery.js' +import { getPrimaryModel } from './providerModels.js' import { getProviderValidationError } from './providerValidation.js' import { maskSecretForDisplay, diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 09b068c5..13081f45 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -590,6 +590,20 @@ describe('getProviderPresetDefaults', () => { expect(defaults.baseUrl).toBe('http://127.0.0.1:1337/v1') expect(defaults.requiresApiKey).toBe(false) }) + + test('deepseek preset defaults to DeepSeek V4 flash and exposes flash/pro aliases', async () => { + const { getProviderPresetDefaults } = await importFreshProviderProfileModules() + + const defaults = getProviderPresetDefaults('deepseek') + + expect(defaults.provider).toBe('openai') + expect(defaults.name).toBe('DeepSeek') + expect(defaults.baseUrl).toBe('https://api.deepseek.com/v1') + expect(defaults.model).toBe( + 'deepseek-v4-flash, deepseek-v4-pro, deepseek-chat, deepseek-reasoner', + ) + expect(defaults.requiresApiKey).toBe(true) + }) }) describe('setActiveProviderProfile', () => { @@ -659,6 +673,45 @@ describe('setActiveProviderProfile', () => { } }) + test('persists primary model for keyed openai-compatible multi-model profiles', async () => { + const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-')) + process.chdir(tempDir) + + try { + const { setActiveProviderProfile } = + await importFreshProviderProfileModules() + const deepSeekProfile = buildProfile({ + id: 'deepseek_prof', + name: 'DeepSeek', + provider: 'openai', + baseUrl: 'https://api.deepseek.com/v1', + model: 'deepseek-v4-flash, deepseek-v4-pro, deepseek-chat', + apiKey: 'sk-deepseek-live', + }) + + saveMockGlobalConfig(current => ({ + ...current, + providerProfiles: [deepSeekProfile], + })) + + const result = setActiveProviderProfile('deepseek_prof') + const persisted = JSON.parse( + readFileSync(join(tempDir, '.openclaude-profile.json'), 'utf8'), + ) + + expect(result?.id).toBe('deepseek_prof') + expect(persisted.profile).toBe('openai') + expect(persisted.env).toEqual({ + OPENAI_BASE_URL: 'https://api.deepseek.com/v1', + OPENAI_MODEL: 'deepseek-v4-flash', + OPENAI_API_KEY: 'sk-deepseek-live', + }) + } finally { + process.chdir(originalCwd) + rmSync(tempDir, { recursive: true, force: true }) + } + }) + test('sets ANTHROPIC_MODEL env var when switching to an anthropic-type provider', async () => { const { setActiveProviderProfile } = await importFreshProviderProfileModules() diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 363c25e1..06ef2a46 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -174,7 +174,7 @@ export function getProviderPresetDefaults( provider: 'openai', name: 'DeepSeek', baseUrl: 'https://api.deepseek.com/v1', - model: 'deepseek-chat', + model: 'deepseek-v4-flash, deepseek-v4-pro, deepseek-chat, deepseek-reasoner', apiKey: '', requiresApiKey: true, } @@ -839,7 +839,7 @@ export function persistActiveProviderProfileModel( /** * Generate model options from a provider profile's model field. - * Each comma-separated model becomes a separate option in the picker. + * Each parsed model becomes a separate option in the picker. */ export function getProfileModelOptions(profile: ProviderProfile): ModelOption[] { const models = parseModelList(profile.model) diff --git a/src/utils/thinking.ts b/src/utils/thinking.ts index 3ca9f610..824a3f60 100644 --- a/src/utils/thinking.ts +++ b/src/utils/thinking.ts @@ -105,6 +105,12 @@ export function modelSupportsThinking(model: string): boolean { if (provider === 'foundry' || provider === 'firstParty') { return !canonical.includes('claude-3-') } + if ( + canonical.startsWith('deepseek-v4-') || + canonical === 'deepseek-reasoner' + ) { + return true + } // 3P (Bedrock/Vertex): only Opus 4+ and Sonnet 4+ return canonical.includes('sonnet-4') || canonical.includes('opus-4') }