Skip to content

Commit 4d2de51

Browse files
authored
Fix provider profile startup precedence (Gitlawb#1157)
Prevent the legacy .openclaude-profile.json fallback from overriding startup env when a modern configured provider profile has already selected a concrete provider configuration. Thread the configured-profile signal from CLI bootstrap into buildStartupEnvFromProfile(), add a concrete-selection helper for the new guard, and preserve the legacy file as a first-run fallback when startup env is incomplete. Also fix the follow-up falsey-flag regression so disabled CLAUDE_CODE_USE_* values do not count as active startup selections, and add regression tests covering stale legacy overrides, incomplete startup env, and falsey provider flags. Verified with: bun test src/utils/providerProfile.test.ts --test-name-pattern " buildStartupEnvFromProfile\
1 parent 18483e4 commit 4d2de51

3 files changed

Lines changed: 141 additions & 0 deletions

File tree

src/entrypoints/cli.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,8 +108,14 @@ async function main(): Promise<void> {
108108
applySafeConfigEnvironmentVariables()
109109
}
110110

111+
const hasConfiguredProviderProfile = await (async () => {
112+
const { getActiveProviderProfile } = await import('../utils/providerProfiles.js')
113+
return getActiveProviderProfile() !== undefined
114+
})()
115+
111116
const startupEnv = await buildStartupEnvFromProfile({
112117
processEnv: process.env,
118+
hasConfiguredProviderProfile,
113119
})
114120
if (startupEnv !== process.env) {
115121
const startupProfileError = await getProviderValidationError(startupEnv)

src/utils/providerProfile.test.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1125,6 +1125,29 @@ test('buildStartupEnvFromProfile preserves plural-profile env when the legacy fi
11251125
assert.equal(env.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, 'saved_moonshot')
11261126
})
11271127

1128+
test('buildStartupEnvFromProfile ignores the legacy file when a configured provider profile already selected a concrete env', async () => {
1129+
const processEnv: NodeJS.ProcessEnv = {
1130+
CLAUDE_CODE_USE_OPENAI: '1',
1131+
OPENAI_BASE_URL: 'https://api.moonshot.ai/v1',
1132+
OPENAI_MODEL: 'kimi-k2.6',
1133+
}
1134+
1135+
const env = await buildStartupEnvFromProfile({
1136+
persisted: profile('openai', {
1137+
OPENAI_API_KEY: 'sk-stale',
1138+
OPENAI_MODEL: 'Meta-Llama-3.1-70B-Instruct',
1139+
OPENAI_BASE_URL: 'https://api.sambanova.ai/v1',
1140+
}),
1141+
processEnv,
1142+
hasConfiguredProviderProfile: true,
1143+
})
1144+
1145+
assert.equal(env, processEnv)
1146+
assert.equal(env.OPENAI_BASE_URL, 'https://api.moonshot.ai/v1')
1147+
assert.equal(env.OPENAI_MODEL, 'kimi-k2.6')
1148+
assert.equal(env.OPENAI_API_KEY, undefined)
1149+
})
1150+
11281151
test('buildStartupEnvFromProfile falls back to legacy file when plural system has not applied', async () => {
11291152
// Counter-example: first-run user with only the legacy file (no plural
11301153
// active profile yet). The legacy file is the correct source, so the
@@ -1148,6 +1171,48 @@ test('buildStartupEnvFromProfile falls back to legacy file when plural system ha
11481171
assert.equal(env.OPENAI_MODEL, 'gpt-4o')
11491172
})
11501173

1174+
test('buildStartupEnvFromProfile still falls back to the legacy file when configured profiles exist but startup env is incomplete', async () => {
1175+
const processEnv = {
1176+
CLAUDE_CODE_USE_OPENAI: '1',
1177+
}
1178+
1179+
const env = await buildStartupEnvFromProfile({
1180+
persisted: profile('openai', {
1181+
OPENAI_API_KEY: 'sk-legacy',
1182+
OPENAI_MODEL: 'gpt-4o',
1183+
OPENAI_BASE_URL: 'https://api.openai.com/v1',
1184+
}),
1185+
processEnv,
1186+
hasConfiguredProviderProfile: true,
1187+
})
1188+
1189+
assert.notEqual(env, processEnv)
1190+
assert.equal(env.OPENAI_API_KEY, 'sk-legacy')
1191+
assert.equal(env.OPENAI_BASE_URL, 'https://api.openai.com/v1')
1192+
assert.equal(env.OPENAI_MODEL, 'gpt-4o')
1193+
})
1194+
1195+
test('buildStartupEnvFromProfile ignores falsey provider flags when deciding whether a configured profile already selected startup env', async () => {
1196+
const processEnv = {
1197+
CLAUDE_CODE_USE_OPENAI: '0',
1198+
OPENAI_BASE_URL: 'https://api.stale.example/v1',
1199+
OPENAI_MODEL: 'stale-model',
1200+
}
1201+
1202+
const env = await buildStartupEnvFromProfile({
1203+
persisted: profile('openai', {
1204+
OPENAI_API_KEY: 'sk-legacy',
1205+
OPENAI_MODEL: 'gpt-4o',
1206+
OPENAI_BASE_URL: 'https://api.openai.com/v1',
1207+
}),
1208+
processEnv,
1209+
hasConfiguredProviderProfile: true,
1210+
})
1211+
1212+
assert.notEqual(env, processEnv)
1213+
assert.equal(env.OPENAI_API_KEY, 'sk-legacy')
1214+
})
1215+
11511216
test('buildStartupEnvFromProfile treats explicit falsey provider flags as user intent', async () => {
11521217
const processEnv = {
11531218
CLAUDE_CODE_USE_OPENAI: '0',

src/utils/providerProfile.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -975,6 +975,61 @@ export function hasExplicitProviderSelection(
975975
)
976976
}
977977

978+
function hasConcreteProviderSelection(
979+
processEnv: NodeJS.ProcessEnv = process.env,
980+
): boolean {
981+
if (processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1') {
982+
return true
983+
}
984+
985+
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_OPENAI)) {
986+
return (
987+
sanitizeProviderConfigValue(processEnv.OPENAI_BASE_URL) !== undefined ||
988+
sanitizeProviderConfigValue(processEnv.OPENAI_API_BASE) !== undefined ||
989+
normalizeProfileModel(
990+
sanitizeProviderConfigValue(processEnv.OPENAI_MODEL),
991+
) !== undefined
992+
)
993+
}
994+
995+
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GEMINI)) {
996+
return (
997+
sanitizeProviderConfigValue(processEnv.GEMINI_BASE_URL) !== undefined ||
998+
normalizeProfileModel(
999+
sanitizeProviderConfigValue(processEnv.GEMINI_MODEL),
1000+
) !== undefined ||
1001+
sanitizeApiKey(processEnv.GEMINI_API_KEY) !== undefined ||
1002+
sanitizeApiKey(processEnv.GOOGLE_API_KEY) !== undefined
1003+
)
1004+
}
1005+
1006+
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_MISTRAL)) {
1007+
return (
1008+
sanitizeProviderConfigValue(processEnv.MISTRAL_BASE_URL) !== undefined ||
1009+
normalizeProfileModel(
1010+
sanitizeProviderConfigValue(processEnv.MISTRAL_MODEL),
1011+
) !== undefined ||
1012+
sanitizeApiKey(processEnv.MISTRAL_API_KEY) !== undefined
1013+
)
1014+
}
1015+
1016+
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
1017+
return (
1018+
sanitizeApiKey(processEnv.GITHUB_TOKEN) !== undefined ||
1019+
sanitizeApiKey(processEnv.GH_TOKEN) !== undefined ||
1020+
normalizeProfileModel(
1021+
sanitizeProviderConfigValue(processEnv.OPENAI_MODEL),
1022+
) !== undefined
1023+
)
1024+
}
1025+
1026+
return (
1027+
isEnvTruthy(processEnv.CLAUDE_CODE_USE_BEDROCK) ||
1028+
isEnvTruthy(processEnv.CLAUDE_CODE_USE_VERTEX) ||
1029+
isEnvTruthy(processEnv.CLAUDE_CODE_USE_FOUNDRY)
1030+
)
1031+
}
1032+
9781033
export function selectAutoProfile(
9791034
recommendedOllamaModel: string | null,
9801035
): ProviderProfile {
@@ -1423,6 +1478,7 @@ export async function buildStartupEnvFromProfile(options?: {
14231478
persisted?: ProfileFile | null
14241479
goal?: RecommendationGoal
14251480
processEnv?: NodeJS.ProcessEnv
1481+
hasConfiguredProviderProfile?: boolean
14261482
getOllamaChatBaseUrl?: (baseUrl?: string) => string
14271483
resolveOllamaDefaultModel?: (goal: RecommendationGoal) => Promise<string>
14281484
readGeminiAccessToken?: () => string | undefined
@@ -1431,6 +1487,8 @@ export async function buildStartupEnvFromProfile(options?: {
14311487
const persisted = options?.persisted ?? loadProfileFile()
14321488

14331489
const profileManagedEnv = processEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1'
1490+
const hasConfiguredProviderProfile =
1491+
options?.hasConfiguredProviderProfile ?? false
14341492

14351493
// The single-profile file in the user config directory is a
14361494
// first-run / fallback mechanism. The newer plural provider-profile
@@ -1448,6 +1506,18 @@ export async function buildStartupEnvFromProfile(options?: {
14481506
return processEnv
14491507
}
14501508

1509+
// If startup already has a concrete provider selection and the modern
1510+
// plural-profile system is configured, keep trusting that selection.
1511+
// This prevents the legacy single-profile file from becoming a silent
1512+
// third precedence layer when `/provider` profiles or explicit env/flags
1513+
// already chose a provider before startup fallback runs.
1514+
if (
1515+
hasConfiguredProviderProfile &&
1516+
hasConcreteProviderSelection(processEnv)
1517+
) {
1518+
return processEnv
1519+
}
1520+
14511521
if (isEnvTruthy(processEnv.CLAUDE_CODE_USE_GITHUB)) {
14521522
return processEnv
14531523
}

0 commit comments

Comments
 (0)