diff --git a/src/commands.ts b/src/commands.ts index 8cae75452..495cbd210 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -150,6 +150,7 @@ import sandboxToggle from './commands/sandbox-toggle/index.js' import chrome from './commands/chrome/index.js' import stickers from './commands/stickers/index.js' import advisor from './commands/advisor.js' +import provider from './commands/provider.js' import { logError } from './utils/log.js' import { toError } from './utils/errors.js' import { logForDebugging } from './utils/debug.js' @@ -258,6 +259,7 @@ export const INTERNAL_ONLY_COMMANDS = [ const COMMANDS = memoize((): Command[] => [ addDir, advisor, + provider, agents, branch, btw, diff --git a/src/commands/provider.ts b/src/commands/provider.ts new file mode 100644 index 000000000..78080b769 --- /dev/null +++ b/src/commands/provider.ts @@ -0,0 +1,111 @@ +import type { Command } from '../commands.js' +import type { LocalCommandCall } from '../types/command.js' +import { getAPIProvider } from '../utils/model/providers.js' +import { updateSettingsForSource } from '../utils/settings/settings.js' +import { getSettings_DEPRECATED } from '../utils/settings/settings.js' +import { applyConfigEnvironmentVariables } from '../utils/managedEnv.js' + +function getEnvVarForProvider(provider: string): string { + switch (provider) { + case 'bedrock': + return 'CLAUDE_CODE_USE_BEDROCK' + case 'vertex': + return 'CLAUDE_CODE_USE_VERTEX' + case 'foundry': + return 'CLAUDE_CODE_USE_FOUNDRY' + default: + throw new Error(`Unknown provider: ${provider}`) + } +} + +// Get merged env: process.env + settings.env (from userSettings) +function getMergedEnv(): Record { + const settings = getSettings_DEPRECATED() + const merged = { ...process.env } + if (settings?.env) { + Object.assign(merged, settings.env) + } + return merged +} + +const call: LocalCommandCall = async (args, context) => { + const arg = args.trim().toLowerCase() + + // No argument: show current provider + if (!arg) { + const current = getAPIProvider() + return { type: 'text', value: `Current API provider: ${current}` } + } + + // unset - clear settings, fallback to env vars + if (arg === 'unset') { + updateSettingsForSource('userSettings', { modelType: undefined }) + // Also clear all provider-specific env vars to prevent conflicts + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + delete process.env.CLAUDE_CODE_USE_OPENAI + return { type: 'text', value: 'API provider cleared (will use environment variables).' } + } + + // Validate provider + const validProviders = ['anthropic', 'openai', 'bedrock', 'vertex', 'foundry'] + if (!validProviders.includes(arg)) { + return { type: 'text', value: `Invalid provider: ${arg}\nValid: ${validProviders.join(', ')}` } + } + + // Check env vars when switching to openai (including settings.env) + if (arg === 'openai') { + const mergedEnv = getMergedEnv() + const hasKey = !!mergedEnv.OPENAI_API_KEY + const hasUrl = !!mergedEnv.OPENAI_BASE_URL + if (!hasKey || !hasUrl) { + updateSettingsForSource('userSettings', { modelType: 'openai' }) + const missing = [] + if (!hasKey) missing.push('OPENAI_API_KEY') + if (!hasUrl) missing.push('OPENAI_BASE_URL') + return { + type: 'text', + value: `Switched to OpenAI provider.\nWarning: Missing env vars: ${missing.join(', ')}\nConfigure them via /login or set manually.`, + } + } + } + + // Handle different provider types + // - 'anthropic' and 'openai' are stored in settings.json (persistent) + // - 'bedrock', 'vertex', 'foundry' are env-only (do NOT touch settings.json) + if (arg === 'anthropic' || arg === 'openai') { + // Clear any cloud provider env vars to avoid conflicts + delete process.env.CLAUDE_CODE_USE_BEDROCK + delete process.env.CLAUDE_CODE_USE_VERTEX + delete process.env.CLAUDE_CODE_USE_FOUNDRY + // Update settings.json + updateSettingsForSource('userSettings', { modelType: arg }) + // Ensure settings.env gets applied to process.env + applyConfigEnvironmentVariables() + return { type: 'text', value: `API provider set to ${arg}.` } + } else { + // Cloud providers: set env vars only, clear settings.modelType + delete process.env.CLAUDE_CODE_USE_OPENAI + delete process.env.OPENAI_API_KEY + delete process.env.OPENAI_BASE_URL + process.env[getEnvVarForProvider(arg)] = '1' + // Clear settings.json to 'anthropic' to avoid validation errors + updateSettingsForSource('userSettings', { modelType: 'anthropic' }) + // Ensure settings.env gets applied to process.env (in case it contains provider-specific creds) + applyConfigEnvironmentVariables() + return { type: 'text', value: `API provider set to ${arg} (via environment variable).` } + } +} + +const provider = { + type: 'local', + name: 'provider', + description: 'Switch API provider (anthropic/openai/bedrock/vertex/foundry)', + aliases: ['api'], + argumentHint: '[anthropic|openai|bedrock|vertex|foundry|unset]', + supportsNonInteractive: true, + load: () => Promise.resolve({ call }), +} satisfies Command + +export default provider diff --git a/src/utils/model/providers.ts b/src/utils/model/providers.ts index a082cd298..bce43ba92 100644 --- a/src/utils/model/providers.ts +++ b/src/utils/model/providers.ts @@ -8,6 +8,9 @@ export function getAPIProvider(): APIProvider { // 1. Check settings.json modelType field (highest priority) const modelType = getInitialSettings().modelType if (modelType === 'openai') return 'openai' + if (modelType === 'bedrock') return 'bedrock' + if (modelType === 'vertex') return 'vertex' + if (modelType === 'foundry') return 'foundry' // 2. Check environment variables (backward compatibility) return isEnvTruthy(process.env.CLAUDE_CODE_USE_OPENAI)