Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"dev:profile:fast": "bun run scripts/provider-launch.ts auto --fast --bare",
"dev:codex": "bun run scripts/provider-launch.ts codex",
"dev:openai": "bun run scripts/provider-launch.ts openai",
"dev:deepseek": "bun run scripts/provider-launch.ts deepseek",
"dev:gemini": "bun run scripts/provider-launch.ts gemini",
"dev:ollama": "bun run scripts/provider-launch.ts ollama",
"dev:ollama:fast": "bun run scripts/provider-launch.ts ollama --fast --bare",
Expand All @@ -26,6 +27,7 @@
"profile:recommend": "bun run scripts/provider-recommend.ts",
"profile:auto": "bun run scripts/provider-recommend.ts --apply",
"profile:codex": "bun run profile:init -- --provider codex --model codexplan",
"profile:deepseek": "bun run profile:init -- --provider deepseek",
"profile:fast": "bun run profile:init -- --provider ollama --model llama3.2:3b",
"profile:code": "bun run profile:init -- --provider ollama --model qwen2.5-coder:7b",
"dev:fast": "bun run profile:fast && bun run dev:ollama:fast",
Expand Down
18 changes: 17 additions & 1 deletion scripts/provider-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {
buildAtomicChatProfileEnv,
buildCodexProfileEnv,
buildDeepSeekProfileEnv,
buildGeminiProfileEnv,
buildMistralProfileEnv,
buildOllamaProfileEnv,
Expand Down Expand Up @@ -38,7 +39,7 @@ function parseArg(name: string): string | null {

function parseProviderArg(): ProviderProfile | 'auto' {
const p = parseArg('--provider')?.toLowerCase()
if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini' || p === 'mistral' || p === 'atomic-chat') return p
if (p === 'openai' || p === 'ollama' || p === 'codex' || p === 'gemini' || p === 'mistral' || p === 'deepseek' || p === 'atomic-chat') return p
return 'auto'
}

Expand Down Expand Up @@ -106,6 +107,21 @@ async function main(): Promise<void> {
process.exit(1)
}

env = builtEnv
} else if (selected === 'deepseek') {
const builtEnv = buildDeepSeekProfileEnv({
model: argModel || null,
baseUrl: argBaseUrl || null,
apiKey: argApiKey || null,
processEnv: process.env,
})

if (!builtEnv) {
console.error('DeepSeek profile requires an API key. Use --api-key or set DEEPSEEK_API_KEY/OPENAI_API_KEY.')
console.error('Get a key at: https://platform.deepseek.com/api_keys')
process.exit(1)
}

env = builtEnv
} else if (selected === 'ollama') {
resolvedOllamaModel ??= await resolveOllamaModel(argModel, argBaseUrl, goal)
Expand Down
11 changes: 9 additions & 2 deletions scripts/provider-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ function parseLaunchOptions(argv: string[]): LaunchOptions {
continue
}

if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini' || lower ==='mistral' || lower === 'atomic-chat') && requestedProfile === 'auto') {
if ((lower === 'auto' || lower === 'openai' || lower === 'ollama' || lower === 'codex' || lower === 'gemini' || lower ==='mistral' || lower === 'deepseek' || lower === 'atomic-chat') && requestedProfile === 'auto') {
requestedProfile = lower as ProviderProfile | 'auto'
continue
}
Expand Down Expand Up @@ -126,6 +126,8 @@ function printSummary(profile: ProviderProfile): void {
console.log('Using configured Gemini provider settings.')
} else if (profile === 'mistral') {
console.log('Using configured Mistral provider settings.')
} else if (profile === 'deepseek') {
console.log('Using configured DeepSeek provider settings.')
} else if (profile === 'codex') {
console.log('Using configured Codex/OpenAI-compatible provider settings.')
} else if (profile === 'atomic-chat') {
Expand All @@ -141,7 +143,7 @@ async function main(): Promise<void> {
const options = parseLaunchOptions(process.argv.slice(2))
const requestedProfile = options.requestedProfile
if (!requestedProfile) {
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|mistral|atomic-chat|mistral|auto] [--fast] [--goal <latency|balanced|coding>] [-- <cli args>]')
console.error('Usage: bun run scripts/provider-launch.ts [openai|ollama|codex|gemini|mistral|deepseek|atomic-chat|auto] [--fast] [--goal <latency|balanced|coding>] [-- <cli args>]')
process.exit(1)
}

Expand Down Expand Up @@ -212,6 +214,11 @@ async function main(): Promise<void> {
process.exit(1)
}

if (profile === 'deepseek' && (!env.OPENAI_API_KEY || env.OPENAI_API_KEY === 'SUA_CHAVE')) {
console.error('DeepSeek profile requires a real API key. Run: bun run profile:init -- --provider deepseek --api-key <key>')
process.exit(1)
}

if (profile === 'openai' && (!env.OPENAI_API_KEY || env.OPENAI_API_KEY === 'SUA_CHAVE')) {
console.error('OPENAI_API_KEY is required for openai profile and cannot be SUA_CHAVE. Run: bun run profile:init -- --provider openai --api-key <key>')
process.exit(1)
Expand Down
100 changes: 97 additions & 3 deletions src/commands/provider/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
applySavedProfileToCurrentSession as applySharedProfileToCurrentSession,
buildCodexOAuthProfileEnv as buildSharedCodexOAuthProfileEnv,
buildCodexProfileEnv,
buildDeepSeekProfileEnv,
buildGeminiProfileEnv,
buildMistralProfileEnv,
buildOllamaProfileEnv,
Expand All @@ -39,6 +40,8 @@ import {
DEFAULT_GEMINI_MODEL,
DEFAULT_MISTRAL_BASE_URL,
DEFAULT_MISTRAL_MODEL,
DEFAULT_DEEPSEEK_BASE_URL,
DEFAULT_DEEPSEEK_MODEL,
deleteProfileFile,
loadProfileFile,
maskSecretForDisplay,
Expand Down Expand Up @@ -137,6 +140,8 @@ type Step =
| { name: 'auto-goal' }
| { name: 'auto-detect'; goal: RecommendationGoal }
| { name: 'ollama-detect' }
| { name: 'deepseek-key' }
| { name: 'deepseek-model'; apiKey: string }
| { name: 'openai-key'; defaultModel: string }
| { name: 'openai-base'; apiKey: string; defaultModel: string }
| {
Expand Down Expand Up @@ -315,6 +320,8 @@ export function buildCurrentProviderSummary(options?: {
providerLabel = 'Codex'
} else if (isLocalProviderUrl(request.baseUrl)) {
providerLabel = getLocalOpenAICompatibleProviderLabel(request.baseUrl)
} else if (/api\.deepseek\.com/i.test(request.baseUrl)) {
providerLabel = 'DeepSeek V4'
}

return {
Expand Down Expand Up @@ -404,6 +411,25 @@ function buildSavedProfileSummary(
? 'configured'
: undefined,
}
case 'deepseek':
return {
providerLabel: 'DeepSeek V4',
modelLabel: getSafeDisplayValue(
env.OPENAI_MODEL ?? DEFAULT_DEEPSEEK_MODEL,
process.env,
env,
),
endpointLabel: getSafeDisplayValue(
env.OPENAI_BASE_URL ?? DEFAULT_DEEPSEEK_BASE_URL,
process.env,
env,
),
credentialLabel:
maskSecretForDisplay(env.OPENAI_API_KEY) !== undefined ||
maskSecretForDisplay(env.DEEPSEEK_API_KEY) !== undefined
? 'configured'
: undefined,
}
case 'ollama':
return {
providerLabel: 'Ollama',
Expand Down Expand Up @@ -482,8 +508,8 @@ export function buildProfileSaveMessage(
function buildUsageText(): string {
const summary = buildCurrentProviderSummary()
const availableProviders = isBareMode()
? 'Choose Auto, Ollama, OpenAI-compatible, Gemini, or Codex, then save a provider profile.'
: 'Choose Auto, Ollama, OpenAI-compatible, Gemini, Codex, or Codex OAuth, then save a provider profile.'
? 'Choose Auto, Ollama, DeepSeek V4, OpenAI-compatible, Gemini, or Codex, then save a provider profile.'
: 'Choose Auto, Ollama, DeepSeek V4, OpenAI-compatible, Gemini, Codex, or Codex OAuth, then save a provider profile.'
return [
'Usage: /provider',
'',
Expand Down Expand Up @@ -645,7 +671,12 @@ function ProviderChooser({
label: 'OpenAI-compatible',
value: 'openai',
description:
'GPT-4o, DeepSeek, OpenRouter, Groq, LM Studio, and similar APIs',
'GPT-4o, OpenRouter, Groq, LM Studio, and similar APIs',
},
{
label: 'DeepSeek V4',
value: 'deepseek',
description: 'Use DeepSeek V4 Pro thinking mode with the official API',
},
{
label: 'Gemini',
Expand Down Expand Up @@ -1252,6 +1283,8 @@ export function ProviderWizard({
name: 'openai-key',
defaultModel: defaults.openAIModel,
})
} else if (value === 'deepseek') {
setStep({ name: 'deepseek-key' })
} else if (value === 'gemini') {
setStep({ name: 'gemini-auth-method' })
} else if (value === 'mistral') {
Expand Down Expand Up @@ -1304,6 +1337,67 @@ export function ProviderWizard({
/>
)

case 'deepseek-key':
return (
<TextEntryDialog
resetStateKey={step.name}
title="DeepSeek V4 setup"
subtitle="Step 1 of 2"
description={
process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY
? 'Enter a DeepSeek API key, or leave this blank to reuse the current DEEPSEEK_API_KEY/OPENAI_API_KEY from this session.'
: 'Enter your DeepSeek API key.'
}
initialValue=""
placeholder="sk-..."
mask="*"
allowEmpty={Boolean(process.env.DEEPSEEK_API_KEY || process.env.OPENAI_API_KEY)}
validate={value => {
const candidate =
value.trim() ||
process.env.DEEPSEEK_API_KEY ||
process.env.OPENAI_API_KEY ||
''
return sanitizeApiKey(candidate)
? null
: 'Enter a real DeepSeek API key.'
}}
onSubmit={value => {
const apiKey =
value.trim() ||
process.env.DEEPSEEK_API_KEY ||
process.env.OPENAI_API_KEY ||
''
setStep({ name: 'deepseek-model', apiKey })
}}
onCancel={() => setStep({ name: 'choose' })}
/>
)

case 'deepseek-model':
return (
<TextEntryDialog
resetStateKey={step.name}
title="DeepSeek V4 setup"
subtitle="Step 2 of 2"
description={`Enter a model name. Leave blank for ${DEFAULT_DEEPSEEK_MODEL}.`}
initialValue={DEFAULT_DEEPSEEK_MODEL}
placeholder={DEFAULT_DEEPSEEK_MODEL}
allowEmpty
onSubmit={value => {
const env = buildDeepSeekProfileEnv({
apiKey: step.apiKey,
model: value.trim() || DEFAULT_DEEPSEEK_MODEL,
processEnv: {},
})
if (env) {
finishProfileSave(onDone, 'deepseek', env)
}
}}
onCancel={() => setStep({ name: 'deepseek-key' })}
/>
)

case 'openai-key':
return (
<TextEntryDialog
Expand Down
2 changes: 1 addition & 1 deletion src/components/ProviderManager.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ const PRESET_ORDER = [
'Azure OpenAI',
'Bankr',
'Codex OAuth',
'DeepSeek',
'DeepSeek V4',
'Google Gemini',
'Groq',
'LM Studio',
Expand Down
4 changes: 2 additions & 2 deletions src/components/ProviderManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1364,8 +1364,8 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
: []),
{
value: 'deepseek',
label: 'DeepSeek',
description: 'DeepSeek OpenAI-compatible endpoint',
label: 'DeepSeek V4',
description: 'DeepSeek V4 OpenAI-compatible endpoint',
},
{
value: 'gemini',
Expand Down
2 changes: 1 addition & 1 deletion src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -984,7 +984,7 @@ async function run(): Promise<CommanderCommand> {
return Number.isFinite(n) ? n : undefined;
}).hideHelp()).option('--from-pr [value]', 'Resume a session linked to a PR by PR number/URL, or open interactive picker with optional search term', value => value || true).option('--no-session-persistence', 'Disable session persistence - sessions will not be saved to disk and cannot be resumed (only works with --print)').addOption(new Option('--resume-session-at <message id>', 'When resuming, only messages up to and including the assistant message with <message.id> (use with --resume in print mode)').argParser(String).hideHelp()).addOption(new Option('--rewind-files <user-message-id>', 'Restore files to state at the specified user message and exit (requires --resume)').hideHelp())
// @[MODEL LAUNCH]: Update the example model ID in the --model help text.
.option('--model <model>', `Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6').`).option('--provider <provider>', `AI provider to use (anthropic, openai, gemini, github, bedrock, vertex, ollama). Reads API keys from environment variables.`).addOption(new Option('--effort <level>', `Effort level for the current session (low, medium, high, max)`).argParser((rawValue: string) => {
.option('--model <model>', `Model for the current session. Provide an alias for the latest model (e.g. 'sonnet' or 'opus') or a model's full name (e.g. 'claude-sonnet-4-6').`).option('--provider <provider>', `AI provider to use (anthropic, openai, deepseek, gemini, github, bedrock, vertex, ollama). Reads API keys from environment variables.`).addOption(new Option('--effort <level>', `Effort level for the current session (low, medium, high, max)`).argParser((rawValue: string) => {
const value = rawValue.toLowerCase();
const allowed = ['low', 'medium', 'high', 'max'];
if (!allowed.includes(value)) {
Expand Down
Loading
Loading