From 3e96e6316af7d635bcc98e340aa428161cb7e934 Mon Sep 17 00:00:00 2001 From: AJ Green Date: Fri, 29 May 2026 16:03:31 -0600 Subject: [PATCH 1/2] fix(/provider): add Anthropic option to provider switcher so users can switch back without restarting (#1426) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, once a third-party provider was activated via /provider, the "Set active provider" screen had no way to return to Anthropic — users had to manually edit ~/.openclaude.json and restart. This change: - Adds an "Anthropic / Claude" entry to the "Set active provider" list - Selecting it calls clearActiveProviderProfile() which clears activeProviderProfileId from config and deletes the startup profile file - Wipes all CLAUDE_CODE_USE_* and provider-specific env vars from the running process so the switch takes effect immediately without a restart - Marks the Anthropic option as "(active)" when no third-party profile is set Co-Authored-By: Claude Sonnet 4.6 --- src/components/ProviderManager.tsx | 45 ++++++++++++++++++++++++++++-- src/utils/providerProfile.ts | 2 +- src/utils/providerProfiles.test.ts | 41 +++++++++++++++++++++++++++ src/utils/providerProfiles.ts | 16 +++++++++++ 4 files changed, 101 insertions(+), 3 deletions(-) diff --git a/src/components/ProviderManager.tsx b/src/components/ProviderManager.tsx index cbf09c6f0e..f1de84b96c 100644 --- a/src/components/ProviderManager.tsx +++ b/src/components/ProviderManager.tsx @@ -23,6 +23,7 @@ import { clearPersistedCodexOAuthProfile, clearPersistedXaiOAuthProfile, createProfileFile, + PROFILE_ENV_KEYS, } from '../utils/providerProfile.js' import { clearXaiCredentials, @@ -47,6 +48,7 @@ import { probeRouteReadiness } from '../integrations/discoveryService.js' import { addProviderProfile, applyActiveProviderProfileFromConfig, + clearActiveProviderProfile, deleteProviderProfile, getActiveProviderProfile, getProviderPresetDefaults, @@ -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' @@ -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(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() @@ -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: @@ -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 ( @@ -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': diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index f593200117..7577cfc4e5 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -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', diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index d5cba09d65..9931c1d18c 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 2abb1d879b..b90327b3f2 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 From 23e54e547177c871ac0b1ae5e5d4b3d5c83d8553 Mon Sep 17 00:00:00 2001 From: AJ Green Date: Sat, 30 May 2026 07:48:17 -0600 Subject: [PATCH 2/2] fix(review): address self-review findings in Anthropic provider switch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four bugs caught by code review before external reviewers saw it: 1. mainLoopModel not reset (Critical): add setAppState({ mainLoopModel: null, mainLoopModelForSession: null }) so the session model resets to the Anthropic default instead of keeping the previous provider's model string. 2. refreshProfiles() race (Major): replaced refreshProfiles() with setProfiles(getProviderProfiles(getGlobalConfig())) + setActiveProfileId(undefined). refreshProfiles() queues a microtask that calls getActiveProviderProfile(), which falls back to profiles[0] when activeProviderProfileId is undefined — overwriting setActiveProfileId(undefined) and showing the wrong profile as active. 3. Sentinel flags not cleared (Major): replaced manual PROFILE_ENV_KEYS loop with clearProviderProfileEnvFromProcessEnv(), which also deletes CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED and _ID — keys that trigger profile re-application and were silently left set by the old approach. 4. GitHub token not cleared (Minor): add explicit clearGithubModelsToken() when GITHUB_MODELS_HYDRATED_ENV_MARKER is present, matching the cleanup done by deleteGithubProvider(). Co-Authored-By: Claude Sonnet 4.6 --- src/components/ProviderManager.tsx | 29 ++++++++++++++++++++++------- src/utils/providerProfile.ts | 2 +- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/components/ProviderManager.tsx b/src/components/ProviderManager.tsx index f1de84b96c..8be4f825f0 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, @@ -23,7 +24,6 @@ import { clearPersistedCodexOAuthProfile, clearPersistedXaiOAuthProfile, createProfileFile, - PROFILE_ENV_KEYS, } from '../utils/providerProfile.js' import { clearXaiCredentials, @@ -49,6 +49,7 @@ import { addProviderProfile, applyActiveProviderProfileFromConfig, clearActiveProviderProfile, + clearProviderProfileEnvFromProcessEnv, deleteProviderProfile, getActiveProviderProfile, getProviderPresetDefaults, @@ -1201,15 +1202,29 @@ export function ProviderManager({ mode, onDone }: Props): React.ReactNode { // 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] + // 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) - refreshProfiles() setStatusMessage(`Active provider: ${ANTHROPIC_PROVIDER_LABEL}`) setIsActivating(false) onDone({ diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 7577cfc4e5..f593200117 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -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' -export const PROFILE_ENV_KEYS = [ +const PROFILE_ENV_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_GEMINI',