Skip to content
Merged
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
7 changes: 7 additions & 0 deletions src/entrypoints/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,13 @@ async function main(): Promise<void> {

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')
Expand Down
87 changes: 87 additions & 0 deletions src/utils/providerFlag.test.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
import {
parseProviderFlag,
parseModelFlag,
applyProviderFlag,
applyProviderFlagFromArgs,
applyModelFlagFromArgs,
VALID_PROVIDERS,
} from './providerFlag.js'

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',
Expand All @@ -21,6 +24,8 @@ const ENV_KEYS = [
'BNKR_API_KEY',
'XAI_API_KEY',
'MINIMAX_API_KEY',
'MISTRAL_MODEL',
'ANTHROPIC_MODEL',
]

const originalEnv: Record<string, string | undefined> = {}
Expand All @@ -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',
Expand All @@ -47,6 +53,8 @@ const RESET_KEYS = [
'BNKR_API_KEY',
'XAI_API_KEY',
'MINIMAX_API_KEY',
'MISTRAL_MODEL',
'ANTHROPIC_MODEL',
] as const

beforeEach(() => {
Expand Down Expand Up @@ -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')
})
})
45 changes: 44 additions & 1 deletion src/utils/providerFlag.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -120,6 +120,49 @@ function getRouteDefaults(provider: string): {
}
}

/**
* Apply --model (without --provider) to process.env for the current process only.
*
* Issue #808: `openclaude --model <name>` 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
Expand Down
Loading