diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx index 0f6260bdc..193ac8a34 100644 --- a/src/entrypoints/cli.tsx +++ b/src/entrypoints/cli.tsx @@ -134,6 +134,13 @@ async function main(): Promise { await validateProviderEnvForStartupOrExit() + // #808: --model alone (no --provider) — route to the env var matching the + // active provider before the banner prints so the override is visible. + if (args.includes('--model')) { + const { applyModelFlagFromArgs } = await import('../utils/providerFlag.js') + applyModelFlagFromArgs(args) + } + // Parse --model early so the startup screen can display the override const { eagerParseCliFlag } = await import('../utils/cliArgs.js') const earlyModelFlag = eagerParseCliFlag('--model') diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index 086e6d152..9d60f6c52 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -1,8 +1,10 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' import { parseProviderFlag, + parseModelFlag, applyProviderFlag, applyProviderFlagFromArgs, + applyModelFlagFromArgs, VALID_PROVIDERS, } from './providerFlag.js' @@ -10,6 +12,7 @@ const ENV_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI', 'CLAUDE_CODE_USE_GITHUB', + 'CLAUDE_CODE_USE_MISTRAL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'OPENAI_BASE_URL', @@ -21,6 +24,8 @@ const ENV_KEYS = [ 'BNKR_API_KEY', 'XAI_API_KEY', 'MINIMAX_API_KEY', + 'MISTRAL_MODEL', + 'ANTHROPIC_MODEL', ] const originalEnv: Record = {} @@ -36,6 +41,7 @@ const RESET_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI', 'CLAUDE_CODE_USE_GITHUB', + 'CLAUDE_CODE_USE_MISTRAL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'OPENAI_BASE_URL', @@ -47,6 +53,8 @@ const RESET_KEYS = [ 'BNKR_API_KEY', 'XAI_API_KEY', 'MINIMAX_API_KEY', + 'MISTRAL_MODEL', + 'ANTHROPIC_MODEL', ] as const beforeEach(() => { @@ -389,3 +397,82 @@ describe('applyProviderFlagFromArgs', () => { expect(applyProviderFlagFromArgs(['--model', 'gpt-4o'])).toBeUndefined() }) }) + +// --- parseModelFlag --- + +describe('parseModelFlag', () => { + test('returns model value when --model is present', () => { + expect(parseModelFlag(['--model', 'gpt-4o-mini'])).toBe('gpt-4o-mini') + }) + + test('returns null when --model is absent', () => { + expect(parseModelFlag(['--provider', 'openai'])).toBeNull() + }) + + test('returns null when --model has no value', () => { + expect(parseModelFlag(['--model'])).toBeNull() + }) + + test('returns null when --model value looks like another flag', () => { + expect(parseModelFlag(['--model', '--provider'])).toBeNull() + }) +}) + +// --- applyModelFlagFromArgs (#808) --- + +describe('applyModelFlagFromArgs', () => { + test('is a no-op when --model is absent', () => { + applyModelFlagFromArgs(['--ide']) + expect(process.env.OPENAI_MODEL).toBeUndefined() + expect(process.env.GEMINI_MODEL).toBeUndefined() + expect(process.env.ANTHROPIC_MODEL).toBeUndefined() + }) + + test('is a no-op when --provider is also present (handled by applyProviderFlagFromArgs)', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + applyModelFlagFromArgs(['--provider', 'openai', '--model', 'gpt-4o']) + expect(process.env.OPENAI_MODEL).toBeUndefined() + }) + + test('sets OPENAI_MODEL when CLAUDE_CODE_USE_OPENAI is active', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + applyModelFlagFromArgs(['--model', 'gpt-4o-mini']) + expect(process.env.OPENAI_MODEL).toBe('gpt-4o-mini') + }) + + test('sets GEMINI_MODEL when CLAUDE_CODE_USE_GEMINI is active', () => { + process.env.CLAUDE_CODE_USE_GEMINI = '1' + applyModelFlagFromArgs(['--model', 'gemini-2.0-flash']) + expect(process.env.GEMINI_MODEL).toBe('gemini-2.0-flash') + }) + + test('sets MISTRAL_MODEL when CLAUDE_CODE_USE_MISTRAL is active', () => { + process.env.CLAUDE_CODE_USE_MISTRAL = '1' + applyModelFlagFromArgs(['--model', 'devstral-latest']) + expect(process.env.MISTRAL_MODEL).toBe('devstral-latest') + }) + + test('sets OPENAI_MODEL when CLAUDE_CODE_USE_GITHUB is active', () => { + process.env.CLAUDE_CODE_USE_GITHUB = '1' + applyModelFlagFromArgs(['--model', 'gpt-4.1']) + expect(process.env.OPENAI_MODEL).toBe('gpt-4.1') + }) + + test('falls back to ANTHROPIC_MODEL when no provider flag is set', () => { + applyModelFlagFromArgs(['--model', 'claude-sonnet-4-6']) + expect(process.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6') + }) + + test('overrides an existing *_MODEL value (saved profile override)', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'gpt-4o' + applyModelFlagFromArgs(['--model', 'gpt-4o-mini']) + expect(process.env.OPENAI_MODEL).toBe('gpt-4o-mini') + }) + + test('accepts --model value containing colons (ollama tag syntax)', () => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + applyModelFlagFromArgs(['--model', 'qwen2.5-coder:14b']) + expect(process.env.OPENAI_MODEL).toBe('qwen2.5-coder:14b') + }) +}) diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index 04f0857a1..7b3d720b7 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -90,7 +90,7 @@ export function applyProviderFlagFromArgs( * Extract the value of --model from argv. * Returns null if absent. */ -function parseModelFlag(args: string[]): string | null { +export function parseModelFlag(args: string[]): string | null { const idx = args.indexOf('--model') if (idx === -1) return null const value = args[idx + 1] @@ -120,6 +120,49 @@ function getRouteDefaults(provider: string): { } } +/** + * Apply --model (without --provider) to process.env for the current process only. + * + * Issue #808: `openclaude --model ` should work standalone so users can + * override the session model without reconfiguring a profile or polluting the + * shell with OPENAI_MODEL=... Must run before the startup banner so the + * displayed model matches the flag, and before resolution paths that read the + * provider-specific *_MODEL env var directly. + * + * Routes the value to the env var matching the already-active provider + * (detected from CLAUDE_CODE_USE_* vars set by saved profile or env). Returns + * undefined when --model is absent or --provider is present (that path is + * handled by applyProviderFlagFromArgs). + */ +export function applyModelFlagFromArgs(args: string[]): void { + if (args.includes('--provider')) return + const model = parseModelFlag(args) + if (!model) return + + const useGemini = + process.env.CLAUDE_CODE_USE_GEMINI === '1' || + process.env.CLAUDE_CODE_USE_GEMINI === 'true' + const useMistral = + process.env.CLAUDE_CODE_USE_MISTRAL === '1' || + process.env.CLAUDE_CODE_USE_MISTRAL === 'true' + const useOpenAI = + process.env.CLAUDE_CODE_USE_OPENAI === '1' || + process.env.CLAUDE_CODE_USE_OPENAI === 'true' + const useGithub = + process.env.CLAUDE_CODE_USE_GITHUB === '1' || + process.env.CLAUDE_CODE_USE_GITHUB === 'true' + + if (useGemini) { + process.env.GEMINI_MODEL = model + } else if (useMistral) { + process.env.MISTRAL_MODEL = model + } else if (useOpenAI || useGithub) { + process.env.OPENAI_MODEL = model + } else { + process.env.ANTHROPIC_MODEL = model + } +} + /** * Apply a provider name to process.env. * Sets the required CLAUDE_CODE_USE_* flag and any provider-specific