Add DeepSeek V4 flash/pro support and DeepSeek thinking compatibility (#877)
* Add DeepSeek V4 support and thinking compatibility * Fix DeepSeek profile persistence regression * Align multi-model handling with openai-multi-model
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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<Record<string, unknown>>
|
||||
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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | 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<string, unknown> | undefined
|
||||
|
||||
@@ -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<string, string> = {
|
||||
'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<string, unknown> = {
|
||||
@@ -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<{
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -96,7 +96,13 @@ const OPENAI_CONTEXT_WINDOWS: Record<string, number> = {
|
||||
'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<string, number> = {
|
||||
'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,
|
||||
|
||||
@@ -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'])
|
||||
})
|
||||
|
||||
@@ -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', {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user