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:
Kevin Codex
2026-04-22 00:59:32 +08:00
committed by GitHub
parent a5bfcbbadf
commit 13de4e85df
4 changed files with 195 additions and 35 deletions

View File

@@ -322,6 +322,58 @@ function hasProviderSelectionFlags(
)
}
/**
* A "complete" explicit provider selection = a USE flag AND at least one
* concrete config value that tells us WHERE to route (a base URL) or WHAT
* to run (a model id). A bare `CLAUDE_CODE_USE_OPENAI=1` with nothing else
* is almost always a stale shell export from a previous session, not real
* intent — and if we respect it, we skip the user's saved active profile
* and fall back to hardcoded defaults (gpt-4o / api.openai.com), which is
* the exact bug users report as "my saved provider isn't picked up".
*
* Used to gate whether saved-profile env should override shell state at
* startup. The weaker `hasProviderSelectionFlags` is still used for the
* anthropic-profile conflict check (any flag is a conflict for
* first-party anthropic) and for alignment fingerprinting.
*/
function hasCompleteProviderSelection(
processEnv: NodeJS.ProcessEnv = process.env,
): boolean {
if (!hasProviderSelectionFlags(processEnv)) return false
if (processEnv.CLAUDE_CODE_USE_OPENAI !== undefined) {
return (
trimOrUndefined(processEnv.OPENAI_BASE_URL) !== undefined ||
trimOrUndefined(processEnv.OPENAI_API_BASE) !== undefined ||
trimOrUndefined(processEnv.OPENAI_MODEL) !== undefined
)
}
if (processEnv.CLAUDE_CODE_USE_GEMINI !== undefined) {
return (
trimOrUndefined(processEnv.GEMINI_BASE_URL) !== undefined ||
trimOrUndefined(processEnv.GEMINI_MODEL) !== undefined ||
trimOrUndefined(processEnv.GEMINI_API_KEY) !== undefined ||
trimOrUndefined(processEnv.GOOGLE_API_KEY) !== undefined
)
}
if (processEnv.CLAUDE_CODE_USE_MISTRAL !== undefined) {
return (
trimOrUndefined(processEnv.MISTRAL_BASE_URL) !== undefined ||
trimOrUndefined(processEnv.MISTRAL_MODEL) !== undefined ||
trimOrUndefined(processEnv.MISTRAL_API_KEY) !== undefined
)
}
if (processEnv.CLAUDE_CODE_USE_GITHUB !== undefined) {
return (
trimOrUndefined(processEnv.GITHUB_TOKEN) !== undefined ||
trimOrUndefined(processEnv.GH_TOKEN) !== undefined ||
trimOrUndefined(processEnv.OPENAI_MODEL) !== undefined
)
}
// Bedrock / Vertex / Foundry signal cloud-provider routing in env; treat
// the flag alone as complete (these paths rely on ambient AWS/GCP creds).
return true
}
function hasConflictingProviderFlagsForProfile(
processEnv: NodeJS.ProcessEnv,
profile: ProviderProfile,
@@ -564,9 +616,15 @@ export function applyActiveProviderProfileFromConfig(
processEnv[PROFILE_ENV_APPLIED_FLAG] === '1' &&
trimOrUndefined(processEnv[PROFILE_ENV_APPLIED_ID]) === activeProfile.id
if (!options?.force && (hasProviderSelectionFlags(processEnv) || processEnv[PROFILE_ENV_APPLIED_FLAG] === '1')) {
if (!options?.force && (hasCompleteProviderSelection(processEnv) || processEnv[PROFILE_ENV_APPLIED_FLAG] === '1')) {
// Respect explicit startup provider intent. Auto-heal only when this
// exact active profile previously applied the current env.
// NOTE: we gate on hasCompleteProviderSelection (flag + concrete config)
// rather than hasProviderSelectionFlags alone. A bare CLAUDE_CODE_USE_*=1
// with no BASE_URL/MODEL is almost always a stale shell export, not
// intent — respecting it would skip the saved profile and fall through
// to hardcoded provider defaults, which surfaces as "my saved provider
// isn't being picked up at startup".
if (!isCurrentEnvProfileManaged) {
return undefined
}