import * as fs from 'fs' import * as path from 'path' import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js' import { PROVIDER_PRESETS } from '../config/providerPresets.js' import type { ApiFormat, ProviderAuthStrategy, ProvidersIndex, SavedProvider, } from '../types/provider.js' import { ATTRIBUTION_HEADER_ENV_KEY, attributionHeaderEnvForModel, } from './attributionHeaderPolicy.js' import { OPENAI_CODEX_OAUTH_FILE_ENV_KEY, OPENAI_OAUTH_PROVIDER_ENV_KEY, buildOpenAIOfficialRuntimeEnv, isOpenAIOfficialProviderId, } from './openaiOfficialProvider.js' export const MANAGED_PROVIDER_ENV_KEYS = [ 'ANTHROPIC_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES', 'CLAUDE_CODE_AUTO_COMPACT_WINDOW', ATTRIBUTION_HEADER_ENV_KEY, MODEL_CONTEXT_WINDOWS_ENV_KEY, OPENAI_OAUTH_PROVIDER_ENV_KEY, OPENAI_CODEX_OAUTH_FILE_ENV_KEY, ] as const const CUSTOM_PROVIDER_MODEL_CAPABILITIES = 'thinking,effort,adaptive_thinking,max_effort' const AUTH_ENV_KEYS = new Set(['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN']) function isRecord(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value) } function isProviderModels(value: unknown): value is SavedProvider['models'] { return ( isRecord(value) && typeof value.main === 'string' && typeof value.haiku === 'string' && typeof value.sonnet === 'string' && typeof value.opus === 'string' ) } function isSavedProvider(value: unknown): value is SavedProvider { if (!isRecord(value)) return false const runtimeKind = value.runtimeKind return ( typeof value.id === 'string' && typeof value.presetId === 'string' && typeof value.name === 'string' && typeof value.apiKey === 'string' && typeof value.baseUrl === 'string' && ( runtimeKind === undefined || runtimeKind === 'anthropic_compatible' || runtimeKind === 'openai_oauth' ) && isProviderModels(value.models) ) } export function normalizeModelMapping(models: SavedProvider['models']): SavedProvider['models'] { const main = models.main.trim() return { main, haiku: models.haiku.trim() || main, sonnet: models.sonnet.trim() || main, opus: models.opus.trim() || main, } } export function normalizeSavedProvider(provider: SavedProvider): SavedProvider { return { ...provider, apiFormat: provider.apiFormat ?? 'anthropic', runtimeKind: provider.runtimeKind ?? 'anthropic_compatible', models: normalizeModelMapping(provider.models), } } export function normalizeProvidersIndex(value: unknown): ProvidersIndex | null { if (!isRecord(value) || !Array.isArray(value.providers)) { return null } const { activeProviderId: legacyActiveProviderId, ...rest } = value const providers = value.providers .filter(isSavedProvider) .map((provider) => normalizeSavedProvider(provider)) const rawActiveId = typeof value.activeId === 'string' ? value.activeId : typeof legacyActiveProviderId === 'string' ? legacyActiveProviderId : null const activeId = rawActiveId && ( providers.some((provider) => provider.id === rawActiveId) || isOpenAIOfficialProviderId(rawActiveId) ) ? rawActiveId : null return { ...rest, schemaVersion: typeof value.schemaVersion === 'number' ? value.schemaVersion : 1, activeId, providers, } } export function getPresetDefaultEnv(presetId: string): Record { return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.defaultEnv ?? {} } function omitAuthEnv(env: Record): Record { return Object.fromEntries( Object.entries(env).filter(([key]) => !AUTH_ENV_KEYS.has(key.toUpperCase())), ) } export function getPresetAuthStrategy(presetId: string): ProviderAuthStrategy { return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.authStrategy ?? 'auth_token' } function getPresetModelContextWindows(presetId: string): Record { return PROVIDER_PRESETS.find((preset) => preset.id === presetId)?.modelContextWindows ?? {} } export function buildProviderAuthEnv( provider: SavedProvider, presetDefaultEnv: Record, needsProxy: boolean, ): Record { if (needsProxy) { return { ANTHROPIC_API_KEY: 'proxy-managed' } } const strategy = provider.authStrategy ?? getPresetAuthStrategy(provider.presetId) const key = provider.apiKey || presetDefaultEnv.ANTHROPIC_AUTH_TOKEN || presetDefaultEnv.ANTHROPIC_API_KEY || '' switch (strategy) { case 'api_key': return key ? { ANTHROPIC_API_KEY: key } : {} case 'auth_token': case 'auth_token_empty_api_key': return { ANTHROPIC_API_KEY: '', ...(key ? { ANTHROPIC_AUTH_TOKEN: key } : {}), } case 'dual_same_token': return key ? { ANTHROPIC_API_KEY: key, ANTHROPIC_AUTH_TOKEN: key } : {} case 'dual_dummy': return { ANTHROPIC_API_KEY: 'dummy', ANTHROPIC_AUTH_TOKEN: 'dummy' } } } export function getManagedEnvKeys(): string[] { const keys = new Set(MANAGED_PROVIDER_ENV_KEYS) for (const preset of PROVIDER_PRESETS) { for (const key of Object.keys(preset.defaultEnv ?? {})) { keys.add(key) } } return [...keys] } export function buildProviderManagedEnv( provider: SavedProvider, options?: { proxyPath?: string; serverPort?: number }, ): Record { if (provider.runtimeKind === 'openai_oauth') { return buildOpenAIOfficialRuntimeEnv() } const apiFormat: ApiFormat = provider.apiFormat ?? 'anthropic' const needsProxy = apiFormat !== 'anthropic' const proxyPath = options?.proxyPath ?? '/proxy' const serverPort = options?.serverPort ?? 3456 const baseUrl = needsProxy ? `http://127.0.0.1:${serverPort}${proxyPath}` : provider.baseUrl const models = normalizeModelMapping(provider.models) const modelContextWindows = { ...getPresetModelContextWindows(provider.presetId), ...(provider.modelContextWindows ?? {}), } const presetDefaultEnv = getPresetDefaultEnv(provider.presetId) const customProviderCapabilityEnv = provider.presetId === 'custom' ? { ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES, ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES, ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES: CUSTOM_PROVIDER_MODEL_CAPABILITIES, } : {} return { ...omitAuthEnv(presetDefaultEnv), ...customProviderCapabilityEnv, ...(provider.autoCompactWindow !== undefined && { CLAUDE_CODE_AUTO_COMPACT_WINDOW: String(provider.autoCompactWindow), }), ...(Object.keys(modelContextWindows).length > 0 && { [MODEL_CONTEXT_WINDOWS_ENV_KEY]: JSON.stringify(modelContextWindows), }), ANTHROPIC_BASE_URL: baseUrl, ...buildProviderAuthEnv(provider, presetDefaultEnv, needsProxy), ANTHROPIC_MODEL: models.main, ANTHROPIC_DEFAULT_HAIKU_MODEL: models.haiku, ANTHROPIC_DEFAULT_SONNET_MODEL: models.sonnet, ANTHROPIC_DEFAULT_OPUS_MODEL: models.opus, ...attributionHeaderEnvForModel(models.main), } } export function readActiveProviderManagedEnv( configDir: string, options?: { serverPort?: number }, ): Record | null { try { const raw = fs.readFileSync(path.join(configDir, 'cc-haha', 'providers.json'), 'utf-8') const index = normalizeProvidersIndex(JSON.parse(raw)) if (!index?.activeId) return null if (isOpenAIOfficialProviderId(index.activeId)) { return buildOpenAIOfficialRuntimeEnv() } const provider = index.providers.find((entry) => entry.id === index.activeId) if (!provider) return null return buildProviderManagedEnv(provider, { serverPort: options?.serverPort, }) } catch { return null } } export function mergeActiveProviderManagedEnv( settingsEnv: Record, configDir: string, options?: { serverPort?: number }, ): Record { const activeProviderEnv = readActiveProviderManagedEnv(configDir, options) if (!activeProviderEnv) { return settingsEnv } const cleanedEnv = { ...settingsEnv } for (const key of getManagedEnvKeys()) { delete cleanedEnv[key] } return { ...cleanedEnv, ...activeProviderEnv, } }