diff --git a/src/components/ProviderManager.tsx b/src/components/ProviderManager.tsx index cbf09c6f0..8be4f825f 100644 --- a/src/components/ProviderManager.tsx +++ b/src/components/ProviderManager.tsx @@ -5,6 +5,7 @@ import { Box, Text } from '../ink.js' import { useTerminalSize } from '../hooks/useTerminalSize.js' import { useKeybinding } from '../keybindings/useKeybinding.js' import { useSetAppState } from '../state/AppState.js' +import { getGlobalConfig } from '../utils/config.js' import type { ProviderProfile } from '../utils/config.js' import { clearCodexCredentials, @@ -47,6 +48,8 @@ import { probeRouteReadiness } from '../integrations/discoveryService.js' import { addProviderProfile, applyActiveProviderProfileFromConfig, + clearActiveProviderProfile, + clearProviderProfileEnvFromProcessEnv, deleteProviderProfile, getActiveProviderProfile, getProviderPresetDefaults, @@ -209,6 +212,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' @@ -1192,6 +1197,45 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode { // block the main thread on Windows (antivirus, disk cache, NTFS metadata). await new Promise(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 AND the profile-applied + // sentinel flags (CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED[_ID]) from + // the running process so routing resolves back to Anthropic immediately. + clearProviderProfileEnvFromProcessEnv() + // If GitHub Models was the previous provider its hydrated token lingers + // outside PROFILE_ENV_KEYS — clear it explicitly. + if (process.env[GITHUB_MODELS_HYDRATED_ENV_MARKER]) { + clearGithubModelsToken() + } + clearStartupProviderOverrideFromUserSettings() + // Reset in-session model to the system default (null = use config/env default). + // Other activation paths (GitHub, normal profiles) do the same. + setAppState(prev => ({ + ...prev, + mainLoopModel: null, + mainLoopModelForSession: null, + })) + // Update state directly instead of via refreshProfiles(): refreshProfiles() + // queues a microtask that calls getActiveProviderProfile(), which falls back + // to profiles[0] when activeProviderProfileId is undefined — overwriting the + // setActiveProfileId(undefined) we want here. + setProfiles(getProviderProfiles(getGlobalConfig())) + setActiveProfileId(undefined) + 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() @@ -2287,9 +2331,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: @@ -2309,6 +2354,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 ( @@ -2547,7 +2603,7 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode { profileId => { void activateSelectedProvider(profileId) }, - { includeGithub: true }, + { includeGithub: true, includeAnthropic: true }, ) break case 'select-edit': diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index d5cba09d6..9931c1d18 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -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 } = diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 2abb1d879..b90327b3f 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -13,6 +13,7 @@ import { getPrimaryModel, parseModelList } from './providerModels.js' import { buildCompatibilityProcessEnv, createProfileFile, + deleteProfileFile, saveProfileFile, buildBedrockProfileEnv, buildGeminiProfileEnv, @@ -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