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
45 changes: 43 additions & 2 deletions src/components/ProviderManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
clearPersistedCodexOAuthProfile,
clearPersistedXaiOAuthProfile,
createProfileFile,
PROFILE_ENV_KEYS,
} from '../utils/providerProfile.js'
import {
clearXaiCredentials,
Expand All @@ -47,6 +48,7 @@ import { probeRouteReadiness } from '../integrations/discoveryService.js'
import {
addProviderProfile,
applyActiveProviderProfileFromConfig,
clearActiveProviderProfile,
deleteProviderProfile,
getActiveProviderProfile,
getProviderPresetDefaults,
Expand Down Expand Up @@ -209,6 +211,8 @@ const FORM_STEPS: Array<{
},
]

const ANTHROPIC_PROVIDER_ID = '__anthropic__'
const ANTHROPIC_PROVIDER_LABEL = 'Anthropic / Claude'
const GITHUB_PROVIDER_ID = '__github_models__'
const GITHUB_PROVIDER_LABEL = 'GitHub Models'
const GITHUB_PROVIDER_DEFAULT_MODEL = 'github:copilot'
Expand Down Expand Up @@ -1192,6 +1196,31 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
// block the main thread on Windows (antivirus, disk cache, NTFS metadata).
await new Promise<void>(resolve => queueMicrotask(resolve))

if (profileId === ANTHROPIC_PROVIDER_ID) {
providerLabel = ANTHROPIC_PROVIDER_LABEL
// Clear the active provider profile from config and delete the
// startup profile file so Anthropic is used on next launch too.
clearActiveProviderProfile()
// Wipe all third-party provider env vars from the current process so
// the running session immediately routes back to Anthropic without
// requiring a restart.
for (const key of PROFILE_ENV_KEYS) {
delete process.env[key]
}
clearStartupProviderOverrideFromUserSettings()
setActiveProfileId(undefined)
refreshProfiles()
setStatusMessage(`Active provider: ${ANTHROPIC_PROVIDER_LABEL}`)
setIsActivating(false)
onDone({
action: 'activated',
activeProviderName: ANTHROPIC_PROVIDER_LABEL,
message: `Provider switched to ${ANTHROPIC_PROVIDER_LABEL}`,
})
returnToMenu()
return
}

if (profileId === GITHUB_PROVIDER_ID) {
providerLabel = GITHUB_PROVIDER_LABEL
const githubError = activateGithubProvider()
Expand Down Expand Up @@ -2287,9 +2316,10 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
title: string,
emptyMessage: string,
onSelect: (profileId: string) => void,
options?: { includeGithub?: boolean },
options?: { includeGithub?: boolean; includeAnthropic?: boolean },
): React.ReactNode {
const includeGithub = options?.includeGithub ?? false
const includeAnthropic = options?.includeAnthropic ?? false
const selectOptions = profiles.map(profile => ({
value: profile.id,
label:
Expand All @@ -2309,6 +2339,17 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
})
}

if (includeAnthropic) {
const isAnthropicActive = !activeProfileId && !isGithubActive
selectOptions.push({
value: ANTHROPIC_PROVIDER_ID,
label: isAnthropicActive
? `${ANTHROPIC_PROVIDER_LABEL} (active)`
: ANTHROPIC_PROVIDER_LABEL,
description: 'Use your Anthropic OAuth session or ANTHROPIC_API_KEY',
})
}

if (selectOptions.length === 0) {
return (
<Box flexDirection="column" gap={1}>
Expand Down Expand Up @@ -2547,7 +2588,7 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode {
profileId => {
void activateSelectedProvider(profileId)
},
{ includeGithub: true },
{ includeGithub: true, includeAnthropic: true },
)
break
case 'select-edit':
Expand Down
2 changes: 1 addition & 1 deletion src/utils/providerProfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export const DEFAULT_GEMINI_MODEL = 'gemini-3-flash-preview'
export const DEFAULT_MISTRAL_BASE_URL = 'https://api.mistral.ai/v1'
export const DEFAULT_MISTRAL_MODEL = 'devstral-latest'

const PROFILE_ENV_KEYS = [
export const PROFILE_ENV_KEYS = [
'CLAUDE_CODE_USE_OPENAI',
'CLAUDE_CODE_USE_GITHUB',
'CLAUDE_CODE_USE_GEMINI',
Expand Down
41 changes: 41 additions & 0 deletions src/utils/providerProfiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1864,6 +1864,47 @@ describe('deleteProviderProfile', () => {
})
})

describe('clearActiveProviderProfile', () => {
test('clears activeProviderProfileId from config and removes startup profile file', async () => {
const configDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-config-'))
const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-provider-'))
process.chdir(tempDir)
process.env.CLAUDE_CONFIG_DIR = configDir

try {
const { setActiveProviderProfile, clearActiveProviderProfile } =
await importFreshProviderProfileModules()

const openaiProfile = buildProfile({
id: 'openai_prof',
name: 'OpenAI Provider',
provider: 'openai',
baseUrl: 'https://api.openai.com/v1',
model: 'gpt-4o',
})

saveMockGlobalConfig(current => ({
...current,
providerProfiles: [openaiProfile],
}))

// Activate a third-party provider first
setActiveProviderProfile('openai_prof', { configDir })
expect(String(process.env.CLAUDE_CODE_USE_OPENAI)).toBe('1')
expect(mockConfigState.activeProviderProfileId).toBe('openai_prof')

// Switch back to Anthropic
clearActiveProviderProfile({ configDir })

// Config should no longer have an active provider profile
expect(mockConfigState.activeProviderProfileId).toBeUndefined()
} finally {
rmSync(configDir, { recursive: true, force: true })
rmSync(tempDir, { recursive: true, force: true })
}
})
})

describe('getProfileModelOptions', () => {
test('generates options for multi-model profile', async () => {
const { getProfileModelOptions } =
Expand Down
16 changes: 16 additions & 0 deletions src/utils/providerProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { getPrimaryModel, parseModelList } from './providerModels.js'
import {
buildCompatibilityProcessEnv,
createProfileFile,
deleteProfileFile,
saveProfileFile,
buildBedrockProfileEnv,
buildGeminiProfileEnv,
Expand Down Expand Up @@ -1212,6 +1213,21 @@ export function setActiveProviderProfile(
return activeProfile
}

export function clearActiveProviderProfile(options?: ProfileFileLocation): void {
saveGlobalConfig(config => ({
...config,
activeProviderProfileId: undefined,
openaiAdditionalModelOptionsCache: [],
}))

// Delete the startup profile file so Anthropic is used on next launch too.
try {
deleteProfileFile(options)
} catch {
// Profile file may not exist — that is fine.
}
}

export function deleteProviderProfile(profileId: string): {
removed: boolean
activeProfileId?: string
Expand Down