fix(provider): saved profile ignored when stale CLAUDE_CODE_USE_* in shell (#807)
* fix(provider): saved profile ignored when stale CLAUDE_CODE_USE_* in shell Users reported "my saved /provider profile isn't picked up at startup — the banner shows gpt-4o / api.openai.com even though I saved Moonshot". Root cause: applyActiveProviderProfileFromConfig() bailed out whenever hasProviderSelectionFlags(processEnv) was true — i.e. whenever ANY CLAUDE_CODE_USE_* flag was present. But a bare `CLAUDE_CODE_USE_OPENAI=1` with no paired OPENAI_BASE_URL / OPENAI_MODEL is almost always a stale shell export left over from a prior manual setup, not genuine startup intent. Respecting it skipped the saved profile and let StartupScreen.ts fall through to the hardcoded `gpt-4o` / `https://api.openai.com/v1` defaults — the exact symptom users see. Fix: narrow the guard from "any flag set" to "flag set AND at least one concrete config value (BASE_URL, MODEL, or API_KEY)". A bare stale flag no longer blocks the saved profile. A real shell selection (flag + URL or flag + model) still wins, preserving the "explicit startup intent overrides saved profile" contract. New helper: hasCompleteProviderSelection(env). Per-provider check for a paired concrete value. Bedrock/Vertex/Foundry keep the flag-alone semantic since they rely on ambient AWS/GCP credentials rather than env config. Three new tests cover the bug and the two counter-cases: - bare USE flag → profile applies (fixes the bug) - USE flag + BASE_URL → profile blocked (preserves explicit intent) - USE flag + MODEL → profile blocked (preserves explicit intent) Co-Authored-By: OpenClaude <openclaude@gitlawb.com> * fix(provider): don't overlay stale legacy profile on plural-managed env Second half of the "saved profile not picked up in banner" bug. The prior commit fixed the guard that prevented applyActiveProviderProfileFromConfig() from firing when a stale CLAUDE_CODE_USE_* flag was in the shell. But even when the plural system applies correctly, buildStartupEnvFromProfile() was then loading the legacy .openclaude-profile.json AND overwriting the plural-managed env with whatever that file contained. addProviderProfile() (the call path the /provider preset picker uses) does NOT sync the legacy file, so a user who went: manual setup: CLAUDE_CODE_USE_OPENAI=1 + OPENAI_MODEL=gpt-4o → writes .openclaude-profile.json as { openai, gpt-4o, ... } /provider: add Moonshot preset, mark active → writes plural config; legacy file UNCHANGED would see startup reliably apply Moonshot env first, then get it clobbered by the stale legacy file. Banner shows gpt-4o / api.openai.com while runtime ends up with the correct env via a different code path — exactly the user-reported symptom. Fix: in buildStartupEnvFromProfile, when the plural system has already set env (CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED === '1'), skip the legacy-file overlay entirely and return processEnv unchanged. Legacy is now strictly a first-run / fallback path for users who haven't adopted the plural system. Also removes the stripped-then-rebuilt env construction that was part of the old overlay path — no longer needed. Test updates: - Replaced "lets saved startup profile override profile-managed env" (encoded the old broken behavior) with a regression test that pins the new semantic: plural env survives when legacy is stale. - Added "falls back to legacy when plural hasn't applied" to pin the first-run path still works. Co-Authored-By: OpenClaude <openclaude@gitlawb.com> --------- Co-authored-by: OpenClaude <openclaude@gitlawb.com>
This commit is contained in:
@@ -256,6 +256,83 @@ describe('applyActiveProviderProfileFromConfig', () => {
|
||||
expect(process.env.OPENAI_MODEL).toBe('qwen2.5:3b')
|
||||
})
|
||||
|
||||
test('applies active profile when a bare CLAUDE_CODE_USE_OPENAI flag is stale (no BASE_URL/MODEL)', async () => {
|
||||
// Regression: a leftover `CLAUDE_CODE_USE_OPENAI=1` in the shell with no
|
||||
// paired OPENAI_BASE_URL / OPENAI_MODEL is not a real explicit selection
|
||||
// — it's a stale export. The previous guard treated it as intent and
|
||||
// skipped the saved profile, causing the startup banner to show hardcoded
|
||||
// defaults (gpt-4o @ api.openai.com) instead of the user's active
|
||||
// profile.
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
delete process.env.OPENAI_BASE_URL
|
||||
delete process.env.OPENAI_API_BASE
|
||||
delete process.env.OPENAI_MODEL
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [
|
||||
buildProfile({
|
||||
id: 'saved_moonshot',
|
||||
baseUrl: 'https://api.moonshot.ai/v1',
|
||||
model: 'kimi-k2.6',
|
||||
}),
|
||||
],
|
||||
activeProviderProfileId: 'saved_moonshot',
|
||||
} as any)
|
||||
|
||||
expect(applied?.id).toBe('saved_moonshot')
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('https://api.moonshot.ai/v1')
|
||||
expect(process.env.OPENAI_MODEL).toBe('kimi-k2.6')
|
||||
})
|
||||
|
||||
test('still respects complete shell selection with USE flag + BASE_URL', async () => {
|
||||
// Counter-example: when the user really did set both the flag AND a
|
||||
// concrete BASE_URL, that IS explicit intent and wins over the saved
|
||||
// profile. This preserves the original "explicit startup wins" semantic.
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_BASE_URL = 'http://192.168.1.1:8080/v1'
|
||||
delete process.env.OPENAI_MODEL
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [
|
||||
buildProfile({
|
||||
id: 'saved_moonshot',
|
||||
baseUrl: 'https://api.moonshot.ai/v1',
|
||||
model: 'kimi-k2.6',
|
||||
}),
|
||||
],
|
||||
activeProviderProfileId: 'saved_moonshot',
|
||||
} as any)
|
||||
|
||||
expect(applied).toBeUndefined()
|
||||
expect(process.env.OPENAI_BASE_URL).toBe('http://192.168.1.1:8080/v1')
|
||||
})
|
||||
|
||||
test('still respects complete shell selection with USE flag + MODEL', async () => {
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
process.env.CLAUDE_CODE_USE_OPENAI = '1'
|
||||
process.env.OPENAI_MODEL = 'gpt-4o-mini'
|
||||
delete process.env.OPENAI_BASE_URL
|
||||
|
||||
const applied = applyActiveProviderProfileFromConfig({
|
||||
providerProfiles: [
|
||||
buildProfile({
|
||||
id: 'saved_moonshot',
|
||||
baseUrl: 'https://api.moonshot.ai/v1',
|
||||
model: 'kimi-k2.6',
|
||||
}),
|
||||
],
|
||||
activeProviderProfileId: 'saved_moonshot',
|
||||
} as any)
|
||||
|
||||
expect(applied).toBeUndefined()
|
||||
expect(process.env.OPENAI_MODEL).toBe('gpt-4o-mini')
|
||||
})
|
||||
|
||||
test('does not override explicit startup selection when profile marker is stale', async () => {
|
||||
const { applyActiveProviderProfileFromConfig } =
|
||||
await importFreshProviderProfileModules()
|
||||
|
||||
Reference in New Issue
Block a user