* feat: add xAI as official provider - Add xAI preset to ProviderManager (alphabetical order) - Add xAI provider detection via XAI_API_KEY - Add xAI startup screen heuristic (x.ai base URL or grok model) - Add xAI status display properties - Add grok-4 and grok-3 context windows - Add xAI model fallbacks across all tiers - Fix JSDoc priority order in providerAutoDetect Co-Authored-By: Claude Opus 4.6 <noreply@openclaude.dev> * fix(xai): persist relaunch classification for xAI profiles Addresses reviewer feedback on feat/xai-official-provider: - isProcessEnvAlignedWithProfile now validates XAI_API_KEY for x.ai base URLs, mirroring the Bankr pattern. Without this, relaunch skips re-applying the profile, XAI_API_KEY stays unset, and getAPIProvider() falls back to 'openai'. - buildOpenAICompatibleStartupEnv now sets XAI_API_KEY when syncing active xAI profile to the legacy fallback file. - Adds 'xai' to VALID_PROVIDERS and --provider xai CLI flag support. - Adds xAI detection to providerDiscovery label heuristics. - Adds 'xai' to legacy ProviderProfile type/isProviderProfile guard. - Adds targeted tests for relaunch alignment, flag application, and discovery labeling. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@openclaude.dev> Co-authored-by: OpenClaude <openclaude@gitlawb.com>
1115 lines
32 KiB
TypeScript
1115 lines
32 KiB
TypeScript
import { existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { resolve } from 'node:path'
|
|
import {
|
|
DEFAULT_CODEX_BASE_URL,
|
|
DEFAULT_OPENAI_BASE_URL,
|
|
isCodexBaseUrl,
|
|
resolveCodexApiCredentials,
|
|
resolveProviderRequest,
|
|
} from '../services/api/providerConfig.js'
|
|
import { parseChatgptAccountId } from '../services/api/codexOAuthShared.js'
|
|
import {
|
|
getGoalDefaultOpenAIModel,
|
|
normalizeRecommendationGoal,
|
|
type RecommendationGoal,
|
|
} from './providerRecommendation.js'
|
|
import { readGeminiAccessToken } from './geminiCredentials.js'
|
|
import { getOllamaChatBaseUrl } from './providerDiscovery.js'
|
|
import { getPrimaryModel } from './providerModels.js'
|
|
import { getProviderValidationError } from './providerValidation.js'
|
|
import {
|
|
maskSecretForDisplay,
|
|
redactSecretValueForDisplay,
|
|
sanitizeApiKey,
|
|
sanitizeProviderConfigValue,
|
|
} from './providerSecrets.js'
|
|
|
|
export {
|
|
maskSecretForDisplay,
|
|
redactSecretValueForDisplay,
|
|
sanitizeApiKey,
|
|
sanitizeProviderConfigValue,
|
|
} from './providerSecrets.js'
|
|
import { isEnvTruthy } from './envUtils.ts'
|
|
|
|
import { PROVIDERS } from './configConstants.js'
|
|
|
|
export const PROFILE_FILE_NAME = '.openclaude-profile.json'
|
|
export const DEFAULT_GEMINI_BASE_URL =
|
|
'https://generativelanguage.googleapis.com/v1beta/openai'
|
|
export const DEFAULT_GEMINI_MODEL = 'gemini-2.0-flash'
|
|
export const DEFAULT_MISTRAL_BASE_URL = 'https://api.mistral.ai/v1'
|
|
export const DEFAULT_MISTRAL_MODEL = 'devstral-latest'
|
|
|
|
const PROFILE_ENV_KEYS = [
|
|
'CLAUDE_CODE_USE_OPENAI',
|
|
'CLAUDE_CODE_USE_GITHUB',
|
|
'CLAUDE_CODE_USE_GEMINI',
|
|
'CLAUDE_CODE_USE_MISTRAL',
|
|
'CLAUDE_CODE_USE_BEDROCK',
|
|
'CLAUDE_CODE_USE_VERTEX',
|
|
'CLAUDE_CODE_USE_FOUNDRY',
|
|
'OPENAI_BASE_URL',
|
|
'OPENAI_MODEL',
|
|
'OPENAI_API_FORMAT',
|
|
'OPENAI_AUTH_HEADER',
|
|
'OPENAI_AUTH_SCHEME',
|
|
'OPENAI_AUTH_HEADER_VALUE',
|
|
'OPENAI_API_KEY',
|
|
'CODEX_API_KEY',
|
|
'CODEX_CREDENTIAL_SOURCE',
|
|
'CHATGPT_ACCOUNT_ID',
|
|
'CODEX_ACCOUNT_ID',
|
|
'GEMINI_API_KEY',
|
|
'GEMINI_AUTH_MODE',
|
|
'GEMINI_ACCESS_TOKEN',
|
|
'GEMINI_MODEL',
|
|
'GEMINI_BASE_URL',
|
|
'GOOGLE_API_KEY',
|
|
'NVIDIA_NIM',
|
|
'NVIDIA_API_KEY',
|
|
'NVIDIA_MODEL',
|
|
'MINIMAX_API_KEY',
|
|
'MINIMAX_BASE_URL',
|
|
'MINIMAX_MODEL',
|
|
'MISTRAL_BASE_URL',
|
|
'MISTRAL_API_KEY',
|
|
'MISTRAL_MODEL',
|
|
'BANKR_BASE_URL',
|
|
'BNKR_API_KEY',
|
|
'BANKR_MODEL',
|
|
'XAI_API_KEY',
|
|
] as const
|
|
|
|
const SECRET_ENV_KEYS = [
|
|
'OPENAI_API_KEY',
|
|
'OPENAI_AUTH_HEADER_VALUE',
|
|
'CODEX_API_KEY',
|
|
'GEMINI_API_KEY',
|
|
'GOOGLE_API_KEY',
|
|
'NVIDIA_API_KEY',
|
|
'MINIMAX_API_KEY',
|
|
'MISTRAL_API_KEY',
|
|
'BNKR_API_KEY',
|
|
'XAI_API_KEY',
|
|
] as const
|
|
|
|
export type ProviderProfile = 'openai' | 'ollama' | 'codex' | 'gemini' | 'atomic-chat' | 'nvidia-nim' | 'minimax' | 'mistral' | 'xai'
|
|
|
|
export type ProfileEnv = {
|
|
OPENAI_BASE_URL?: string
|
|
OPENAI_MODEL?: string
|
|
OPENAI_API_FORMAT?: 'chat_completions' | 'responses'
|
|
OPENAI_AUTH_HEADER?: string
|
|
OPENAI_AUTH_SCHEME?: 'bearer' | 'raw'
|
|
OPENAI_AUTH_HEADER_VALUE?: string
|
|
OPENAI_API_KEY?: string
|
|
CODEX_API_KEY?: string
|
|
CODEX_CREDENTIAL_SOURCE?: 'oauth' | 'existing'
|
|
CHATGPT_ACCOUNT_ID?: string
|
|
CODEX_ACCOUNT_ID?: string
|
|
GEMINI_API_KEY?: string
|
|
GEMINI_AUTH_MODE?: 'api-key' | 'access-token' | 'adc'
|
|
GEMINI_MODEL?: string
|
|
GEMINI_BASE_URL?: string
|
|
GOOGLE_API_KEY?: string
|
|
NVIDIA_NIM?: string
|
|
NVIDIA_API_KEY?: string
|
|
MINIMAX_API_KEY?: string
|
|
MINIMAX_BASE_URL?: string
|
|
MINIMAX_MODEL?: string
|
|
MISTRAL_BASE_URL?: string
|
|
MISTRAL_API_KEY?: string
|
|
MISTRAL_MODEL?: string
|
|
BANKR_BASE_URL?: string
|
|
BNKR_API_KEY?: string
|
|
BANKR_MODEL?: string
|
|
XAI_API_KEY?: string
|
|
}
|
|
|
|
export type ProfileFile = {
|
|
profile: ProviderProfile
|
|
env: ProfileEnv
|
|
createdAt: string
|
|
}
|
|
|
|
type SecretValueSource = Partial<
|
|
Record<
|
|
| 'OPENAI_API_KEY'
|
|
| 'OPENAI_AUTH_HEADER_VALUE'
|
|
| 'CODEX_API_KEY'
|
|
| 'GEMINI_API_KEY'
|
|
| 'GOOGLE_API_KEY'
|
|
| 'NVIDIA_API_KEY'
|
|
| 'MINIMAX_API_KEY'
|
|
| 'MISTRAL_API_KEY'
|
|
| 'BNKR_API_KEY',
|
|
string | undefined
|
|
>
|
|
>
|
|
|
|
type ProfileFileLocation = {
|
|
cwd?: string
|
|
filePath?: string
|
|
}
|
|
|
|
function resolveProfileFilePath(options?: ProfileFileLocation): string {
|
|
if (options?.filePath) {
|
|
return options.filePath
|
|
}
|
|
|
|
return resolve(options?.cwd ?? process.cwd(), PROFILE_FILE_NAME)
|
|
}
|
|
|
|
function normalizeProfileModel(
|
|
value: string | undefined | null,
|
|
): string | undefined {
|
|
const trimmed = value?.trim()
|
|
if (!trimmed) {
|
|
return undefined
|
|
}
|
|
|
|
const primary = getPrimaryModel(trimmed)
|
|
return primary.length > 0 ? primary : undefined
|
|
}
|
|
|
|
export function isProviderProfile(value: unknown): value is ProviderProfile {
|
|
return (
|
|
value === 'openai' ||
|
|
value === 'ollama' ||
|
|
value === 'codex' ||
|
|
value === 'gemini' ||
|
|
value === 'atomic-chat' ||
|
|
value === 'nvidia-nim' ||
|
|
value === 'minimax' ||
|
|
value === 'mistral' ||
|
|
value === 'xai'
|
|
)
|
|
}
|
|
|
|
export function buildOllamaProfileEnv(
|
|
model: string,
|
|
options: {
|
|
baseUrl?: string | null
|
|
getOllamaChatBaseUrl: (baseUrl?: string) => string
|
|
},
|
|
): ProfileEnv {
|
|
return {
|
|
OPENAI_BASE_URL: options.getOllamaChatBaseUrl(options.baseUrl ?? undefined),
|
|
OPENAI_MODEL: model,
|
|
}
|
|
}
|
|
|
|
export function buildAtomicChatProfileEnv(
|
|
model: string,
|
|
options: {
|
|
baseUrl?: string | null
|
|
getAtomicChatChatBaseUrl: (baseUrl?: string) => string
|
|
},
|
|
): ProfileEnv {
|
|
return {
|
|
OPENAI_BASE_URL: options.getAtomicChatChatBaseUrl(options.baseUrl ?? undefined),
|
|
OPENAI_MODEL: model,
|
|
}
|
|
}
|
|
|
|
export function buildNvidiaNimProfileEnv(options: {
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const key = sanitizeApiKey(options.apiKey ?? processEnv.NVIDIA_API_KEY)
|
|
if (!key) {
|
|
return null
|
|
}
|
|
|
|
const defaultBaseUrl = 'https://integrate.api.nvidia.com/v1'
|
|
const secretSource: SecretValueSource = { OPENAI_API_KEY: key }
|
|
|
|
return {
|
|
OPENAI_BASE_URL:
|
|
sanitizeProviderConfigValue(options.baseUrl, secretSource) ||
|
|
sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource) ||
|
|
defaultBaseUrl,
|
|
OPENAI_MODEL:
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(options.model, secretSource),
|
|
) ||
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(processEnv.OPENAI_MODEL, secretSource),
|
|
) ||
|
|
'nvidia/llama-3.1-nemotron-70b-instruct',
|
|
OPENAI_API_KEY: key,
|
|
NVIDIA_NIM: '1',
|
|
}
|
|
}
|
|
|
|
export function buildMiniMaxProfileEnv(options: {
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const key = sanitizeApiKey(options.apiKey ?? processEnv.MINIMAX_API_KEY)
|
|
if (!key) {
|
|
return null
|
|
}
|
|
|
|
const defaultBaseUrl = 'https://api.minimax.io/v1'
|
|
const defaultModel = 'MiniMax-M2.5'
|
|
const secretSource: SecretValueSource = { OPENAI_API_KEY: key }
|
|
|
|
return {
|
|
OPENAI_BASE_URL:
|
|
sanitizeProviderConfigValue(options.baseUrl, secretSource) ||
|
|
sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL, secretSource) ||
|
|
defaultBaseUrl,
|
|
OPENAI_MODEL:
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(options.model, secretSource),
|
|
) ||
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(processEnv.OPENAI_MODEL, secretSource),
|
|
) ||
|
|
defaultModel,
|
|
OPENAI_API_KEY: key,
|
|
MINIMAX_API_KEY: key,
|
|
MINIMAX_BASE_URL: defaultBaseUrl,
|
|
MINIMAX_MODEL: defaultModel,
|
|
}
|
|
}
|
|
|
|
export function buildGeminiProfileEnv(options: {
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
authMode?: 'api-key' | 'access-token' | 'adc'
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const authMode = options.authMode ?? 'api-key'
|
|
const key = sanitizeApiKey(
|
|
options.apiKey ??
|
|
processEnv.GEMINI_API_KEY ??
|
|
processEnv.GOOGLE_API_KEY,
|
|
)
|
|
if (authMode === 'api-key' && !key) {
|
|
return null
|
|
}
|
|
|
|
const secretSource: SecretValueSource = key ? { GEMINI_API_KEY: key } : {}
|
|
|
|
const env: ProfileEnv = {
|
|
GEMINI_AUTH_MODE: authMode,
|
|
GEMINI_MODEL:
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(options.model, secretSource),
|
|
) ||
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(processEnv.GEMINI_MODEL, secretSource),
|
|
) ||
|
|
DEFAULT_GEMINI_MODEL,
|
|
}
|
|
|
|
if (authMode === 'api-key' && key) {
|
|
env.GEMINI_API_KEY = key
|
|
}
|
|
|
|
const baseUrl =
|
|
sanitizeProviderConfigValue(options.baseUrl, secretSource) ||
|
|
sanitizeProviderConfigValue(processEnv.GEMINI_BASE_URL, secretSource)
|
|
if (baseUrl) {
|
|
env.GEMINI_BASE_URL = baseUrl
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
export function buildOpenAIProfileEnv(options: {
|
|
goal: RecommendationGoal
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
apiFormat?: 'chat_completions' | 'responses' | null
|
|
authHeader?: string | null
|
|
authScheme?: 'bearer' | 'raw' | null
|
|
authHeaderValue?: string | null
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const key = sanitizeApiKey(options.apiKey ?? processEnv.OPENAI_API_KEY)
|
|
const authHeaderValue = sanitizeApiKey(
|
|
options.authHeaderValue ?? processEnv.OPENAI_AUTH_HEADER_VALUE,
|
|
)
|
|
if (!key && !authHeaderValue) {
|
|
return null
|
|
}
|
|
|
|
const defaultModel = getGoalDefaultOpenAIModel(options.goal)
|
|
const secretSource: SecretValueSource = {
|
|
OPENAI_API_KEY: key,
|
|
OPENAI_AUTH_HEADER_VALUE: authHeaderValue,
|
|
}
|
|
const shellOpenAIModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
processEnv.OPENAI_MODEL,
|
|
secretSource,
|
|
),
|
|
)
|
|
const shellOpenAIBaseUrl = sanitizeProviderConfigValue(
|
|
processEnv.OPENAI_BASE_URL,
|
|
secretSource,
|
|
)
|
|
const shellOpenAIRequest = resolveProviderRequest({
|
|
model: shellOpenAIModel,
|
|
baseUrl: shellOpenAIBaseUrl,
|
|
fallbackModel: defaultModel,
|
|
apiFormat: processEnv.OPENAI_API_FORMAT,
|
|
})
|
|
const useShellOpenAIConfig = shellOpenAIRequest.transport !== 'codex_responses'
|
|
|
|
return {
|
|
OPENAI_BASE_URL:
|
|
sanitizeProviderConfigValue(options.baseUrl, secretSource) ||
|
|
(useShellOpenAIConfig ? shellOpenAIBaseUrl : undefined) ||
|
|
DEFAULT_OPENAI_BASE_URL,
|
|
OPENAI_MODEL:
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(options.model, secretSource),
|
|
) ||
|
|
(useShellOpenAIConfig ? shellOpenAIModel : undefined) ||
|
|
defaultModel,
|
|
...(options.apiFormat ? { OPENAI_API_FORMAT: options.apiFormat } : {}),
|
|
...(options.authHeader ? { OPENAI_AUTH_HEADER: options.authHeader } : {}),
|
|
...(options.authScheme ? { OPENAI_AUTH_SCHEME: options.authScheme } : {}),
|
|
...(authHeaderValue ? { OPENAI_AUTH_HEADER_VALUE: authHeaderValue } : {}),
|
|
...(key ? { OPENAI_API_KEY: key } : {}),
|
|
}
|
|
}
|
|
|
|
export function buildCodexProfileEnv(options: {
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
credentialSource?: 'oauth' | 'existing'
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const key = sanitizeApiKey(options.apiKey ?? processEnv.CODEX_API_KEY)
|
|
const credentialEnv = key
|
|
? ({ ...processEnv, CODEX_API_KEY: key } as NodeJS.ProcessEnv)
|
|
: processEnv
|
|
const credentials = resolveCodexApiCredentials(credentialEnv)
|
|
if (!credentials.apiKey || !credentials.accountId) {
|
|
return null
|
|
}
|
|
const credentialSource =
|
|
options.credentialSource ??
|
|
(credentials.source === 'secure-storage' ? 'oauth' : 'existing')
|
|
|
|
const env: ProfileEnv = {
|
|
OPENAI_BASE_URL: options.baseUrl || DEFAULT_CODEX_BASE_URL,
|
|
OPENAI_MODEL: options.model || 'codexplan',
|
|
CODEX_CREDENTIAL_SOURCE: credentialSource,
|
|
}
|
|
|
|
if (key) {
|
|
env.CODEX_API_KEY = key
|
|
}
|
|
|
|
env.CHATGPT_ACCOUNT_ID = credentials.accountId
|
|
|
|
return env
|
|
}
|
|
|
|
export function buildMistralProfileEnv(options: {
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const key = sanitizeApiKey(options.apiKey ?? processEnv.MISTRAL_API_KEY)
|
|
if (!key) {
|
|
return null
|
|
}
|
|
|
|
const env: ProfileEnv = {
|
|
MISTRAL_API_KEY: key,
|
|
MISTRAL_MODEL:
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(options.model, { MISTRAL_API_KEY: key }),
|
|
) ||
|
|
normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
processEnv.MISTRAL_MODEL,
|
|
{ MISTRAL_API_KEY: key },
|
|
),
|
|
) ||
|
|
DEFAULT_MISTRAL_MODEL,
|
|
}
|
|
|
|
const baseUrl =
|
|
sanitizeProviderConfigValue(options.baseUrl, { MISTRAL_API_KEY: key }) ||
|
|
sanitizeProviderConfigValue(
|
|
processEnv.MISTRAL_BASE_URL,
|
|
{ MISTRAL_API_KEY: key },
|
|
)
|
|
if (baseUrl) {
|
|
env.MISTRAL_BASE_URL = baseUrl
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
export function buildBankrProfileEnv(options: {
|
|
model?: string | null
|
|
baseUrl?: string | null
|
|
apiKey?: string | null
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): ProfileEnv | null {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const key = sanitizeApiKey(options.apiKey ?? processEnv.BNKR_API_KEY)
|
|
if (!key) {
|
|
return null
|
|
}
|
|
|
|
const env: ProfileEnv = {
|
|
BNKR_API_KEY: key,
|
|
BANKR_MODEL:
|
|
sanitizeProviderConfigValue(options.model, { BNKR_API_KEY: key }) ||
|
|
sanitizeProviderConfigValue(
|
|
processEnv.BANKR_MODEL,
|
|
{ BNKR_API_KEY: key },
|
|
) ||
|
|
'claude-opus-4.6',
|
|
}
|
|
|
|
const baseUrl =
|
|
sanitizeProviderConfigValue(options.baseUrl, { BNKR_API_KEY: key }) ||
|
|
sanitizeProviderConfigValue(
|
|
processEnv.BANKR_BASE_URL,
|
|
{ BNKR_API_KEY: key },
|
|
)
|
|
if (baseUrl) {
|
|
env.BANKR_BASE_URL = baseUrl
|
|
}
|
|
|
|
return env
|
|
}
|
|
|
|
export function buildCodexOAuthProfileEnv(
|
|
tokens: {
|
|
accessToken: string
|
|
idToken?: string
|
|
accountId?: string
|
|
},
|
|
): ProfileEnv | null {
|
|
const accountId =
|
|
tokens.accountId ??
|
|
parseChatgptAccountId(tokens.idToken) ??
|
|
parseChatgptAccountId(tokens.accessToken)
|
|
|
|
if (!accountId) {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
OPENAI_BASE_URL: DEFAULT_CODEX_BASE_URL,
|
|
OPENAI_MODEL: 'codexplan',
|
|
CHATGPT_ACCOUNT_ID: accountId,
|
|
CODEX_CREDENTIAL_SOURCE: 'oauth',
|
|
}
|
|
}
|
|
|
|
export function createProfileFile(
|
|
profile: ProviderProfile,
|
|
env: ProfileEnv,
|
|
): ProfileFile {
|
|
return {
|
|
profile,
|
|
env,
|
|
createdAt: new Date().toISOString(),
|
|
}
|
|
}
|
|
|
|
export function isPersistedCodexOAuthProfile(
|
|
persisted: ProfileFile | null,
|
|
): boolean {
|
|
return (
|
|
persisted?.profile === 'codex' &&
|
|
persisted.env.CODEX_CREDENTIAL_SOURCE === 'oauth'
|
|
)
|
|
}
|
|
|
|
export function clearPersistedCodexOAuthProfile(
|
|
options?: ProfileFileLocation,
|
|
): string | null {
|
|
const persisted = loadProfileFile(options)
|
|
if (!isPersistedCodexOAuthProfile(persisted)) {
|
|
return null
|
|
}
|
|
|
|
return deleteProfileFile(options)
|
|
}
|
|
|
|
export function loadProfileFile(options?: ProfileFileLocation): ProfileFile | null {
|
|
const filePath = resolveProfileFilePath(options)
|
|
if (!existsSync(filePath)) {
|
|
return null
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(readFileSync(filePath, 'utf8')) as Partial<ProfileFile>
|
|
if (!isProviderProfile(parsed.profile) || !parsed.env || typeof parsed.env !== 'object') {
|
|
return null
|
|
}
|
|
|
|
return {
|
|
profile: parsed.profile,
|
|
env: parsed.env,
|
|
createdAt:
|
|
typeof parsed.createdAt === 'string'
|
|
? parsed.createdAt
|
|
: new Date().toISOString(),
|
|
}
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export function saveProfileFile(
|
|
profileFile: ProfileFile,
|
|
options?: ProfileFileLocation,
|
|
): string {
|
|
const filePath = resolveProfileFilePath(options)
|
|
writeFileSync(filePath, JSON.stringify(profileFile, null, 2), {
|
|
encoding: 'utf8',
|
|
mode: 0o600,
|
|
})
|
|
return filePath
|
|
}
|
|
|
|
export function deleteProfileFile(options?: ProfileFileLocation): string {
|
|
const filePath = resolveProfileFilePath(options)
|
|
rmSync(filePath, { force: true })
|
|
return filePath
|
|
}
|
|
|
|
export function hasExplicitProviderSelection(
|
|
processEnv: NodeJS.ProcessEnv = process.env,
|
|
): boolean {
|
|
// If env was already applied from a provider profile, preserve it.
|
|
if (processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1') {
|
|
return true
|
|
}
|
|
|
|
return (
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI) ||
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB) ||
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI) ||
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL) ||
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_BEDROCK) ||
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_VERTEX) ||
|
|
isEnvTruthy(processEnv.CLAUDE_CODE_USE_FOUNDRY)
|
|
)
|
|
}
|
|
|
|
export function selectAutoProfile(
|
|
recommendedOllamaModel: string | null,
|
|
): ProviderProfile {
|
|
return recommendedOllamaModel ? 'ollama' : 'openai'
|
|
}
|
|
|
|
export async function buildLaunchEnv(options: {
|
|
profile: ProviderProfile
|
|
persisted: ProfileFile | null
|
|
goal: RecommendationGoal
|
|
processEnv?: NodeJS.ProcessEnv
|
|
getOllamaChatBaseUrl?: (baseUrl?: string) => string
|
|
resolveOllamaDefaultModel?: (goal: RecommendationGoal) => Promise<string>
|
|
getAtomicChatChatBaseUrl?: (baseUrl?: string) => string
|
|
resolveAtomicChatDefaultModel?: () => Promise<string | null>
|
|
readGeminiAccessToken?: () => string | undefined
|
|
}): Promise<NodeJS.ProcessEnv> {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const persistedEnv =
|
|
options.persisted?.profile === options.profile
|
|
? options.persisted.env ?? {}
|
|
: {}
|
|
const persistedOpenAIModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
persistedEnv.OPENAI_MODEL,
|
|
persistedEnv,
|
|
),
|
|
)
|
|
const persistedOpenAIBaseUrl = sanitizeProviderConfigValue(
|
|
persistedEnv.OPENAI_BASE_URL,
|
|
persistedEnv,
|
|
)
|
|
const persistedOpenAIApiFormat = persistedEnv.OPENAI_API_FORMAT
|
|
const persistedOpenAIAuthHeader = persistedEnv.OPENAI_AUTH_HEADER
|
|
const persistedOpenAIAuthScheme = persistedEnv.OPENAI_AUTH_SCHEME
|
|
const persistedOpenAIAuthHeaderValue = sanitizeApiKey(
|
|
persistedEnv.OPENAI_AUTH_HEADER_VALUE,
|
|
)
|
|
const shellOpenAIModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
processEnv.OPENAI_MODEL,
|
|
processEnv as SecretValueSource,
|
|
),
|
|
)
|
|
const shellOpenAIBaseUrl = sanitizeProviderConfigValue(
|
|
processEnv.OPENAI_BASE_URL,
|
|
processEnv as SecretValueSource,
|
|
)
|
|
const persistedGeminiModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
persistedEnv.GEMINI_MODEL,
|
|
persistedEnv,
|
|
),
|
|
)
|
|
const persistedGeminiBaseUrl = sanitizeProviderConfigValue(
|
|
persistedEnv.GEMINI_BASE_URL,
|
|
persistedEnv,
|
|
)
|
|
const shellGeminiModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
processEnv.GEMINI_MODEL,
|
|
processEnv as SecretValueSource,
|
|
),
|
|
)
|
|
const shellGeminiBaseUrl = sanitizeProviderConfigValue(
|
|
processEnv.GEMINI_BASE_URL,
|
|
processEnv as SecretValueSource,
|
|
)
|
|
const shellGeminiAccessToken =
|
|
processEnv.GEMINI_ACCESS_TOKEN?.trim() || undefined
|
|
const storedGeminiAccessToken =
|
|
options.readGeminiAccessToken?.() ?? readGeminiAccessToken()
|
|
|
|
const shellGeminiKey = sanitizeApiKey(
|
|
processEnv.GEMINI_API_KEY ?? processEnv.GOOGLE_API_KEY,
|
|
)
|
|
const persistedGeminiKey = sanitizeApiKey(persistedEnv.GEMINI_API_KEY)
|
|
const persistedGeminiAuthMode = persistedEnv.GEMINI_AUTH_MODE
|
|
|
|
if (hasExplicitProviderSelection(processEnv)) {
|
|
for (let provider of PROVIDERS) {
|
|
if (provider === "anthropic") {
|
|
continue;
|
|
}
|
|
|
|
const env_key_name = `CLAUDE_CODE_USE_${provider.toUpperCase()}`
|
|
|
|
if (env_key_name in processEnv && isEnvTruthy(processEnv[env_key_name])) {
|
|
options.profile = provider;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (options.profile === 'gemini') {
|
|
const env: NodeJS.ProcessEnv = {
|
|
...processEnv,
|
|
CLAUDE_CODE_USE_GEMINI: '1',
|
|
}
|
|
|
|
delete env.CLAUDE_CODE_USE_OPENAI
|
|
delete env.CLAUDE_CODE_USE_GITHUB
|
|
delete env.CODEX_CREDENTIAL_SOURCE
|
|
|
|
env.GEMINI_MODEL =
|
|
shellGeminiModel ||
|
|
persistedGeminiModel ||
|
|
DEFAULT_GEMINI_MODEL
|
|
env.GEMINI_BASE_URL =
|
|
shellGeminiBaseUrl ||
|
|
persistedGeminiBaseUrl ||
|
|
DEFAULT_GEMINI_BASE_URL
|
|
|
|
const geminiAuthMode =
|
|
persistedGeminiAuthMode === 'access-token' ||
|
|
persistedGeminiAuthMode === 'adc'
|
|
? persistedGeminiAuthMode
|
|
: 'api-key'
|
|
const geminiKey = shellGeminiKey || persistedGeminiKey
|
|
if (geminiAuthMode === 'api-key' && geminiKey) {
|
|
env.GEMINI_API_KEY = geminiKey
|
|
} else {
|
|
delete env.GEMINI_API_KEY
|
|
}
|
|
env.GEMINI_AUTH_MODE = geminiAuthMode
|
|
if (geminiAuthMode === 'access-token') {
|
|
const geminiAccessToken =
|
|
shellGeminiAccessToken || storedGeminiAccessToken
|
|
if (geminiAccessToken) {
|
|
env.GEMINI_ACCESS_TOKEN = geminiAccessToken
|
|
} else {
|
|
delete env.GEMINI_ACCESS_TOKEN
|
|
}
|
|
} else {
|
|
delete env.GEMINI_ACCESS_TOKEN
|
|
}
|
|
|
|
delete env.GOOGLE_API_KEY
|
|
delete env.OPENAI_BASE_URL
|
|
delete env.OPENAI_MODEL
|
|
delete env.OPENAI_API_FORMAT
|
|
delete env.OPENAI_AUTH_HEADER
|
|
delete env.OPENAI_AUTH_SCHEME
|
|
delete env.OPENAI_AUTH_HEADER_VALUE
|
|
delete env.OPENAI_API_KEY
|
|
delete env.CODEX_API_KEY
|
|
delete env.CHATGPT_ACCOUNT_ID
|
|
delete env.CODEX_ACCOUNT_ID
|
|
|
|
return env
|
|
}
|
|
|
|
if (options.profile === 'mistral') {
|
|
const env: NodeJS.ProcessEnv = {
|
|
...processEnv,
|
|
CLAUDE_CODE_USE_MISTRAL: '1',
|
|
}
|
|
|
|
delete env.CLAUDE_CODE_USE_OPENAI
|
|
delete env.CLAUDE_CODE_USE_GITHUB
|
|
delete env.CLAUDE_CODE_USE_GEMINI
|
|
delete env.CLAUDE_CODE_USE_BEDROCK
|
|
delete env.CLAUDE_CODE_USE_VERTEX
|
|
delete env.CLAUDE_CODE_USE_FOUNDRY
|
|
|
|
const shellMistralModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
processEnv.MISTRAL_MODEL,
|
|
),
|
|
)
|
|
const persistedMistralModel = normalizeProfileModel(
|
|
sanitizeProviderConfigValue(
|
|
persistedEnv.MISTRAL_MODEL,
|
|
),
|
|
)
|
|
const shellMistralBaseUrl = sanitizeProviderConfigValue(
|
|
processEnv.MISTRAL_BASE_URL,
|
|
)
|
|
const persistedMistralBaseUrl = sanitizeProviderConfigValue(
|
|
persistedEnv.MISTRAL_BASE_URL,
|
|
)
|
|
|
|
env.MISTRAL_MODEL =
|
|
shellMistralModel || persistedMistralModel || DEFAULT_MISTRAL_MODEL
|
|
|
|
const shellMistralKey = sanitizeApiKey(
|
|
processEnv.MISTRAL_API_KEY,
|
|
)
|
|
const persistedMistralKey = sanitizeApiKey(persistedEnv.MISTRAL_API_KEY)
|
|
const mistralKey = shellMistralKey || persistedMistralKey
|
|
|
|
if (mistralKey) {
|
|
env.MISTRAL_API_KEY = mistralKey
|
|
} else {
|
|
delete env.MISTRAL_API_KEY
|
|
}
|
|
|
|
if (shellMistralBaseUrl || persistedMistralBaseUrl) {
|
|
env.MISTRAL_BASE_URL = shellMistralBaseUrl || persistedMistralBaseUrl
|
|
} else {
|
|
delete env.MISTRAL_BASE_URL
|
|
}
|
|
|
|
delete env.GEMINI_API_KEY
|
|
delete env.GEMINI_AUTH_MODE
|
|
delete env.GEMINI_ACCESS_TOKEN
|
|
delete env.GEMINI_MODEL
|
|
delete env.GEMINI_BASE_URL
|
|
delete env.GOOGLE_API_KEY
|
|
delete env.OPENAI_BASE_URL
|
|
delete env.OPENAI_MODEL
|
|
delete env.OPENAI_API_FORMAT
|
|
delete env.OPENAI_AUTH_HEADER
|
|
delete env.OPENAI_AUTH_SCHEME
|
|
delete env.OPENAI_AUTH_HEADER_VALUE
|
|
delete env.OPENAI_API_KEY
|
|
delete env.CODEX_API_KEY
|
|
delete env.CHATGPT_ACCOUNT_ID
|
|
delete env.CODEX_ACCOUNT_ID
|
|
|
|
return env
|
|
}
|
|
|
|
const env: NodeJS.ProcessEnv = {
|
|
...processEnv,
|
|
CLAUDE_CODE_USE_OPENAI: '1',
|
|
}
|
|
|
|
delete env.CLAUDE_CODE_USE_MISTRAL
|
|
delete env.CLAUDE_CODE_USE_BEDROCK
|
|
delete env.CLAUDE_CODE_USE_VERTEX
|
|
delete env.CLAUDE_CODE_USE_FOUNDRY
|
|
delete env.CLAUDE_CODE_USE_GEMINI
|
|
delete env.CLAUDE_CODE_USE_GITHUB
|
|
delete env.CODEX_CREDENTIAL_SOURCE
|
|
delete env.GEMINI_API_KEY
|
|
delete env.GEMINI_AUTH_MODE
|
|
delete env.GEMINI_ACCESS_TOKEN
|
|
delete env.GEMINI_MODEL
|
|
delete env.GEMINI_BASE_URL
|
|
delete env.GOOGLE_API_KEY
|
|
|
|
if (options.profile === 'ollama') {
|
|
const getOllamaBaseUrl =
|
|
options.getOllamaChatBaseUrl ?? (() => 'http://localhost:11434/v1')
|
|
const resolveOllamaModel =
|
|
options.resolveOllamaDefaultModel ?? (async () => 'llama3.1:8b')
|
|
|
|
env.OPENAI_BASE_URL = persistedOpenAIBaseUrl || getOllamaBaseUrl()
|
|
env.OPENAI_MODEL =
|
|
persistedOpenAIModel ||
|
|
(await resolveOllamaModel(options.goal))
|
|
|
|
delete env.OPENAI_API_KEY
|
|
delete env.OPENAI_API_FORMAT
|
|
delete env.OPENAI_AUTH_HEADER
|
|
delete env.OPENAI_AUTH_SCHEME
|
|
delete env.OPENAI_AUTH_HEADER_VALUE
|
|
delete env.CODEX_API_KEY
|
|
delete env.CHATGPT_ACCOUNT_ID
|
|
delete env.CODEX_ACCOUNT_ID
|
|
|
|
return env
|
|
}
|
|
|
|
if (options.profile === 'atomic-chat') {
|
|
const getAtomicChatBaseUrl =
|
|
options.getAtomicChatChatBaseUrl ?? (() => 'http://127.0.0.1:1337/v1')
|
|
const resolveModel =
|
|
options.resolveAtomicChatDefaultModel ?? (async () => null as string | null)
|
|
|
|
env.OPENAI_BASE_URL = persistedEnv.OPENAI_BASE_URL || getAtomicChatBaseUrl()
|
|
env.OPENAI_MODEL =
|
|
persistedEnv.OPENAI_MODEL ||
|
|
(await resolveModel()) ||
|
|
''
|
|
|
|
delete env.OPENAI_API_KEY
|
|
delete env.OPENAI_API_FORMAT
|
|
delete env.OPENAI_AUTH_HEADER
|
|
delete env.OPENAI_AUTH_SCHEME
|
|
delete env.OPENAI_AUTH_HEADER_VALUE
|
|
delete env.CODEX_API_KEY
|
|
delete env.CHATGPT_ACCOUNT_ID
|
|
delete env.CODEX_ACCOUNT_ID
|
|
|
|
return env
|
|
}
|
|
|
|
if (options.profile === 'codex') {
|
|
env.OPENAI_BASE_URL =
|
|
persistedOpenAIBaseUrl && isCodexBaseUrl(persistedOpenAIBaseUrl)
|
|
? persistedOpenAIBaseUrl
|
|
: DEFAULT_CODEX_BASE_URL
|
|
env.OPENAI_MODEL = persistedOpenAIModel || 'codexplan'
|
|
delete env.OPENAI_API_KEY
|
|
delete env.OPENAI_API_FORMAT
|
|
delete env.OPENAI_AUTH_HEADER
|
|
delete env.OPENAI_AUTH_SCHEME
|
|
delete env.OPENAI_AUTH_HEADER_VALUE
|
|
|
|
const codexKey =
|
|
sanitizeApiKey(processEnv.CODEX_API_KEY) ||
|
|
sanitizeApiKey(persistedEnv.CODEX_API_KEY)
|
|
const liveCodexCredentials = resolveCodexApiCredentials(processEnv)
|
|
const codexAccountId =
|
|
processEnv.CHATGPT_ACCOUNT_ID ||
|
|
processEnv.CODEX_ACCOUNT_ID ||
|
|
liveCodexCredentials.accountId ||
|
|
persistedEnv.CHATGPT_ACCOUNT_ID ||
|
|
persistedEnv.CODEX_ACCOUNT_ID
|
|
if (codexKey) {
|
|
env.CODEX_API_KEY = codexKey
|
|
} else {
|
|
delete env.CODEX_API_KEY
|
|
}
|
|
|
|
if (codexAccountId) {
|
|
env.CHATGPT_ACCOUNT_ID = codexAccountId
|
|
} else {
|
|
delete env.CHATGPT_ACCOUNT_ID
|
|
}
|
|
delete env.CODEX_ACCOUNT_ID
|
|
|
|
return env
|
|
}
|
|
|
|
const defaultOpenAIModel = getGoalDefaultOpenAIModel(options.goal)
|
|
const shellOpenAIRequest = resolveProviderRequest({
|
|
model: shellOpenAIModel,
|
|
baseUrl: shellOpenAIBaseUrl,
|
|
fallbackModel: defaultOpenAIModel,
|
|
apiFormat: processEnv.OPENAI_API_FORMAT,
|
|
})
|
|
const persistedOpenAIRequest = resolveProviderRequest({
|
|
model: persistedOpenAIModel,
|
|
baseUrl: persistedOpenAIBaseUrl,
|
|
fallbackModel: defaultOpenAIModel,
|
|
apiFormat: persistedOpenAIApiFormat,
|
|
})
|
|
const useShellOpenAIConfig = shellOpenAIRequest.transport !== 'codex_responses'
|
|
const usePersistedOpenAIConfig =
|
|
(!persistedOpenAIModel && !persistedOpenAIBaseUrl) ||
|
|
persistedOpenAIRequest.transport !== 'codex_responses'
|
|
|
|
env.OPENAI_BASE_URL =
|
|
(useShellOpenAIConfig ? shellOpenAIBaseUrl : undefined) ||
|
|
(usePersistedOpenAIConfig ? persistedOpenAIBaseUrl : undefined) ||
|
|
DEFAULT_OPENAI_BASE_URL
|
|
env.OPENAI_MODEL =
|
|
(useShellOpenAIConfig ? shellOpenAIModel : undefined) ||
|
|
(usePersistedOpenAIConfig ? persistedOpenAIModel : undefined) ||
|
|
defaultOpenAIModel
|
|
const openAIApiFormat =
|
|
processEnv.OPENAI_API_FORMAT ||
|
|
(usePersistedOpenAIConfig ? persistedOpenAIApiFormat : undefined)
|
|
if (openAIApiFormat) {
|
|
env.OPENAI_API_FORMAT = openAIApiFormat
|
|
} else {
|
|
delete env.OPENAI_API_FORMAT
|
|
}
|
|
const openAIAuthHeader =
|
|
processEnv.OPENAI_AUTH_HEADER ||
|
|
(usePersistedOpenAIConfig ? persistedOpenAIAuthHeader : undefined)
|
|
if (openAIAuthHeader) {
|
|
env.OPENAI_AUTH_HEADER = openAIAuthHeader
|
|
} else {
|
|
delete env.OPENAI_AUTH_HEADER
|
|
}
|
|
const openAIAuthScheme =
|
|
processEnv.OPENAI_AUTH_SCHEME ||
|
|
(usePersistedOpenAIConfig ? persistedOpenAIAuthScheme : undefined)
|
|
if (openAIAuthScheme) {
|
|
env.OPENAI_AUTH_SCHEME = openAIAuthScheme
|
|
} else {
|
|
delete env.OPENAI_AUTH_SCHEME
|
|
}
|
|
const openAIAuthHeaderValue =
|
|
sanitizeApiKey(processEnv.OPENAI_AUTH_HEADER_VALUE) ||
|
|
(usePersistedOpenAIConfig ? persistedOpenAIAuthHeaderValue : undefined)
|
|
if (openAIAuthHeaderValue) {
|
|
env.OPENAI_AUTH_HEADER_VALUE = openAIAuthHeaderValue
|
|
} else {
|
|
delete env.OPENAI_AUTH_HEADER_VALUE
|
|
}
|
|
const openAIKey = processEnv.OPENAI_API_KEY || persistedEnv.OPENAI_API_KEY
|
|
if (openAIKey) {
|
|
env.OPENAI_API_KEY = openAIKey
|
|
} else {
|
|
delete env.OPENAI_API_KEY
|
|
}
|
|
delete env.CODEX_API_KEY
|
|
delete env.CHATGPT_ACCOUNT_ID
|
|
delete env.CODEX_ACCOUNT_ID
|
|
return env
|
|
}
|
|
|
|
export async function buildStartupEnvFromProfile(options?: {
|
|
persisted?: ProfileFile | null
|
|
goal?: RecommendationGoal
|
|
processEnv?: NodeJS.ProcessEnv
|
|
getOllamaChatBaseUrl?: (baseUrl?: string) => string
|
|
resolveOllamaDefaultModel?: (goal: RecommendationGoal) => Promise<string>
|
|
readGeminiAccessToken?: () => string | undefined
|
|
}): Promise<NodeJS.ProcessEnv> {
|
|
const processEnv = options?.processEnv ?? process.env
|
|
const persisted = options?.persisted ?? loadProfileFile()
|
|
|
|
const profileManagedEnv = processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1'
|
|
|
|
// The legacy single-profile file (~/.openclaude-profile.json) is a
|
|
// first-run / fallback mechanism. The newer plural provider-profile
|
|
// system (`/provider` presets + activeProviderProfileId in config) is
|
|
// applied earlier in the bootstrap via applyActiveProviderProfileFromConfig
|
|
// and signals completion with CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED=1.
|
|
//
|
|
// If the plural system has already set env, trust it — do NOT overlay the
|
|
// legacy file. addProviderProfile() does not sync the legacy file, so a
|
|
// stale legacy file (e.g. OpenAI defaults from an earlier manual setup)
|
|
// would otherwise overwrite the correct plural env and surface as the
|
|
// "banner shows gpt-4o / api.openai.com even though my saved profile is
|
|
// Moonshot" bug.
|
|
if (profileManagedEnv) {
|
|
return processEnv
|
|
}
|
|
|
|
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
|
|
return processEnv
|
|
}
|
|
|
|
if (!persisted) {
|
|
return processEnv
|
|
}
|
|
|
|
return buildLaunchEnv({
|
|
profile: persisted.profile,
|
|
persisted,
|
|
goal:
|
|
options?.goal ??
|
|
normalizeRecommendationGoal(processEnv.OPENCLAUDE_PROFILE_GOAL),
|
|
processEnv,
|
|
getOllamaChatBaseUrl:
|
|
options?.getOllamaChatBaseUrl ?? getOllamaChatBaseUrl,
|
|
resolveOllamaDefaultModel: options?.resolveOllamaDefaultModel,
|
|
readGeminiAccessToken: options?.readGeminiAccessToken,
|
|
})
|
|
}
|
|
|
|
export function applyProfileEnvToProcessEnv(
|
|
targetEnv: NodeJS.ProcessEnv,
|
|
nextEnv: NodeJS.ProcessEnv,
|
|
): void {
|
|
for (const key of PROFILE_ENV_KEYS) {
|
|
delete targetEnv[key]
|
|
}
|
|
|
|
Object.assign(targetEnv, nextEnv)
|
|
}
|
|
|
|
export async function applySavedProfileToCurrentSession(options: {
|
|
profileFile: ProfileFile
|
|
processEnv?: NodeJS.ProcessEnv
|
|
}): Promise<string | null> {
|
|
const processEnv = options.processEnv ?? process.env
|
|
const baseEnv = { ...processEnv }
|
|
const isCodexOAuthProfile =
|
|
options.profileFile.profile === 'codex' &&
|
|
options.profileFile.env.CODEX_CREDENTIAL_SOURCE === 'oauth'
|
|
|
|
delete baseEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
|
|
delete baseEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID
|
|
if (isCodexOAuthProfile) {
|
|
delete baseEnv.CODEX_API_KEY
|
|
delete baseEnv.CODEX_ACCOUNT_ID
|
|
delete baseEnv.CHATGPT_ACCOUNT_ID
|
|
}
|
|
|
|
const nextEnv = await buildLaunchEnv({
|
|
profile: options.profileFile.profile,
|
|
persisted: options.profileFile,
|
|
goal: normalizeRecommendationGoal(processEnv.OPENCLAUDE_PROFILE_GOAL),
|
|
processEnv: baseEnv,
|
|
getOllamaChatBaseUrl,
|
|
readGeminiAccessToken,
|
|
})
|
|
const validationError = await getProviderValidationError(nextEnv)
|
|
if (validationError) {
|
|
return validationError
|
|
}
|
|
|
|
delete processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED
|
|
delete processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID
|
|
applyProfileEnvToProcessEnv(processEnv, nextEnv)
|
|
return null
|
|
}
|