mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-29 02:01:50 +08:00
fix(codex): keep provider's own key on switch-away backfill
When switching away from a Codex provider, cc-switch backfills the outgoing provider's stored config from the live ~/.codex files so that in-app changes are captured. restore_codex_provider_token_for_backfill decides what to persist as the provider's auth.OPENAI_API_KEY: - When the live config.toml carries a per-provider experimental_bearer_token, that token is provider-scoped and is lifted back into the stored auth. Correct. - When it does NOT (the default preserve_codex_official_auth_on_switch = false mode keeps the active key in the shared auth.json), the function returned early and the caller adopted the live auth.json wholesale as the provider's stored auth. The problem: auth.json is a single-slot shared file with no provider identity; it always holds the most-recently-activated provider's key. Any time it holds ANOTHER provider's key (after a proxy-takeover backup/restore cycle, an in-app ChatGPT login overwriting auth.json, a cloud-sync current/live divergence, or any current-vs-live mismatch), the switch-away backfill overwrote the outgoing provider's stored key with another's. Repeated switches made keys silently converge across providers that share a base URL, surfacing as "model xxx not found" because the wrong key reached a model it was not entitled to. Fix: in the no-bearer branch, preserve the provider's own DB-stored auth instead of adopting the shared live auth.json. Live config.toml changes (model/base_url/mcp/env) are still captured by the caller; only the credential slot is no longer taken from the shared file. This mirrors the #6277 restore-side rule of never letting the shared live credential clobber per-provider storage. Adds two regression tests: - backfill_keeps_provider_own_key_when_live_auth_holds_another_key (the failing case under the old code) - backfill_restores_key_from_live_bearer_token_when_present (positive control for the already-correct bearer path) Fixes #6414.
This commit is contained in:
@@ -2846,9 +2846,20 @@ pub fn prepare_codex_provider_live_config(
|
||||
/// `auth.OPENAI_API_KEY` so the stored provider keeps its canonical shape
|
||||
/// and generated live tokens don't leak into stored provider TOML.
|
||||
///
|
||||
/// Only intervenes when the live config actually carries a bearer token —
|
||||
/// otherwise the function is a no-op so the caller's normal backfill path
|
||||
/// (which keeps live `auth` as the authoritative source) is unaffected.
|
||||
/// When the live config carries a per-provider `experimental_bearer_token`,
|
||||
/// that token is provider-scoped and safe to lift back into the stored auth.
|
||||
///
|
||||
/// When it does NOT (the default `preserve_codex_official_auth_on_switch =
|
||||
/// false` mode keeps the active key in the shared `auth.json` instead), the
|
||||
/// live `auth` is a single-slot file with no provider identity. It may hold a
|
||||
/// *different* provider's key after a backup/restore cycle, an in-app ChatGPT
|
||||
/// login, or any current/live divergence. Adopting it here would overwrite
|
||||
/// this provider's stored key with another's, so keys silently converge across
|
||||
/// providers that share a base URL (#6414). In that case keep the provider's
|
||||
/// own stored `auth` instead — the live `config.toml` changes below the auth
|
||||
/// slot are still captured by the caller. This mirrors the #6277 restore-side
|
||||
/// rule of never letting the shared live credential clobber per-provider
|
||||
/// storage.
|
||||
pub fn restore_codex_provider_token_for_backfill(
|
||||
settings: &mut Value,
|
||||
template_settings: &Value,
|
||||
@@ -2862,6 +2873,18 @@ pub fn restore_codex_provider_token_for_backfill(
|
||||
};
|
||||
|
||||
let Some(token) = extract_codex_experimental_bearer_token(&config_text) else {
|
||||
// No per-provider bearer token: do NOT adopt the shared live
|
||||
// `auth.json` — it may belong to another provider. Preserve this
|
||||
// provider's own stored auth so its key is not silently overwritten.
|
||||
if let Some(obj) = settings.as_object_mut() {
|
||||
if let Some(stored_auth) = template_settings
|
||||
.get("auth")
|
||||
.filter(|value| value.is_object())
|
||||
.cloned()
|
||||
{
|
||||
obj.insert("auth".to_string(), stored_auth);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -3737,6 +3760,78 @@ wire_api = "responses"
|
||||
);
|
||||
}
|
||||
|
||||
/// Regression for #6414: two third-party Codex providers share a base URL
|
||||
/// but use different API keys. In the default `preserve_codex_official_auth_on_switch
|
||||
/// = false` mode the active key lives in the *shared* `auth.json`, not in a
|
||||
/// per-provider `experimental_bearer_token`. When a switch-away backfill reads a
|
||||
/// live `auth.json` that happens to hold ANOTHER provider's key (after a
|
||||
/// backup/restore cycle, an in-app ChatGPT login, or a current/live divergence),
|
||||
/// the outgoing provider's stored key must NOT be overwritten with that other
|
||||
/// key — otherwise keys silently converge across providers.
|
||||
#[test]
|
||||
fn backfill_keeps_provider_own_key_when_live_auth_holds_another_key() {
|
||||
// Provider A's stored auth carries its own key.
|
||||
let template_settings = json!({
|
||||
"auth": { "OPENAI_API_KEY": "sk-A" },
|
||||
"config": "model_provider = \"custom\"\nmodel = \"model-A\"\n"
|
||||
});
|
||||
|
||||
// Live snapshot: shared auth.json holds provider B's key, and the
|
||||
// config has no per-provider experimental_bearer_token (default mode).
|
||||
let mut live_settings = json!({
|
||||
"auth": { "OPENAI_API_KEY": "sk-B" },
|
||||
"config": "model_provider = \"custom\"\nmodel = \"model-A\"\n"
|
||||
});
|
||||
|
||||
restore_codex_settings_for_backfill(&mut live_settings, &template_settings, true)
|
||||
.expect("backfill");
|
||||
|
||||
assert_eq!(
|
||||
live_settings
|
||||
.get("auth")
|
||||
.and_then(|a| a.get("OPENAI_API_KEY"))
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("sk-A"),
|
||||
"backfill must keep the provider's own stored key, not adopt the shared live auth.json"
|
||||
);
|
||||
}
|
||||
|
||||
/// Positive control for #6414: when the live config DOES carry a per-provider
|
||||
/// `experimental_bearer_token`, the backfill still lifts it back into the
|
||||
/// stored `auth.OPENAI_API_KEY` (this path was already correct).
|
||||
#[test]
|
||||
fn backfill_restores_key_from_live_bearer_token_when_present() {
|
||||
let template_settings = json!({
|
||||
"auth": { "OPENAI_API_KEY": "sk-A" },
|
||||
"config": "model_provider = \"custom\"\nmodel = \"model-A\"\n"
|
||||
});
|
||||
|
||||
let mut live_settings = json!({
|
||||
"auth": { "OPENAI_API_KEY": "sk-B" },
|
||||
"config": "model_provider = \"custom\"\nmodel = \"model-A\"\nexperimental_bearer_token = \"sk-A\"\n"
|
||||
});
|
||||
|
||||
restore_codex_settings_for_backfill(&mut live_settings, &template_settings, true)
|
||||
.expect("backfill");
|
||||
|
||||
assert_eq!(
|
||||
live_settings
|
||||
.get("auth")
|
||||
.and_then(|a| a.get("OPENAI_API_KEY"))
|
||||
.and_then(|v| v.as_str()),
|
||||
Some("sk-A"),
|
||||
"backfill must lift the live experimental_bearer_token into the stored auth"
|
||||
);
|
||||
assert!(
|
||||
!live_settings
|
||||
.get("config")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.contains("experimental_bearer_token"),
|
||||
"backfill must strip the lifted bearer token from the stored config"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_url_writes_into_correct_model_provider_section() {
|
||||
let input = r#"model_provider = "any"
|
||||
|
||||
Reference in New Issue
Block a user