修复 Codex 切换供应商后历史记录变化 (#2349)

* Keep Codex history stable across provider switches

* Restore template Codex provider id when backfilling live config

Backfill writes the current Codex live config back to the previous
provider's stored template after a switch. Because the live file now
carries a normalized stable model_provider id, the previous provider's
template would lose its own provider-specific id (and any matching
[profiles.*] references) on every subsequent switch.

Reverse the normalization at backfill time by rewriting model_provider,
the active model_providers section, and matching profile references back
to the template's original id.

---------

Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
SaladDay
2026-04-30 09:54:25 +08:00
committed by GitHub
parent eb304232b3
commit a1e6c3b65d
6 changed files with 1043 additions and 29 deletions
+235
View File
@@ -239,6 +239,241 @@ command = "say"
);
}
#[test]
fn provider_service_switch_codex_preserves_live_model_provider_id_for_history() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let legacy_auth = json!({ "OPENAI_API_KEY": "rightcode-key" });
let legacy_config = r#"model_provider = "rightcode"
model = "gpt-5.4"
[model_providers.rightcode]
name = "RightCode"
base_url = "https://rightcode.example/v1"
wire_api = "responses"
requires_openai_auth = true
"#;
write_codex_live_atomic(&legacy_auth, Some(legacy_config))
.expect("seed existing codex live config");
let mut initial_config = MultiAppConfig::default();
{
let manager = initial_config
.get_manager_mut(&AppType::Codex)
.expect("codex manager");
manager.current = "old-provider".to_string();
manager.providers.insert(
"old-provider".to_string(),
Provider::with_id(
"old-provider".to_string(),
"RightCode".to_string(),
json!({
"auth": {"OPENAI_API_KEY": "stale"},
"config": legacy_config
}),
None,
),
);
manager.providers.insert(
"new-provider".to_string(),
Provider::with_id(
"new-provider".to_string(),
"AiHubMix".to_string(),
json!({
"auth": {"OPENAI_API_KEY": "fresh-key"},
"config": r#"model_provider = "aihubmix"
model = "gpt-5.4"
[model_providers.aihubmix]
name = "AiHubMix"
base_url = "https://aihubmix.example/v1"
wire_api = "responses"
requires_openai_auth = true
"#
}),
None,
),
);
}
let state = create_test_state_with_config(&initial_config).expect("create test state");
ProviderService::switch(&state, AppType::Codex, "new-provider")
.expect("switch provider should succeed");
let config_text =
std::fs::read_to_string(cc_switch_lib::get_codex_config_path()).expect("read config.toml");
let parsed: toml::Value = toml::from_str(&config_text).expect("parse config.toml");
assert_eq!(
parsed.get("model_provider").and_then(|v| v.as_str()),
Some("rightcode"),
"live Codex model_provider should stay stable so resume history remains visible"
);
let model_providers = parsed
.get("model_providers")
.and_then(|v| v.as_table())
.expect("model_providers table exists");
assert!(
model_providers.get("aihubmix").is_none(),
"target provider-specific id should be rewritten in live config"
);
assert_eq!(
model_providers
.get("rightcode")
.and_then(|v| v.get("base_url"))
.and_then(|v| v.as_str()),
Some("https://aihubmix.example/v1"),
"stable provider id should point at the newly selected supplier endpoint"
);
let providers = state
.db
.get_all_providers(AppType::Codex.as_str())
.expect("read providers after switch");
let new_config_text = providers
.get("new-provider")
.expect("new provider exists")
.settings_config
.get("config")
.and_then(|v| v.as_str())
.unwrap_or_default();
assert!(
new_config_text.contains("[model_providers.aihubmix]"),
"stored provider template should remain provider-specific"
);
}
#[test]
fn provider_service_switch_codex_backfill_keeps_provider_specific_model_provider_id() {
let _guard = test_mutex().lock().expect("acquire test mutex");
reset_test_fs();
let _home = ensure_test_home();
let legacy_auth = json!({ "OPENAI_API_KEY": "rightcode-key" });
let provider_a_config = r#"model_provider = "rightcode"
model = "gpt-5.4"
[model_providers.rightcode]
name = "RightCode"
base_url = "https://rightcode.example/v1"
wire_api = "responses"
requires_openai_auth = true
"#;
write_codex_live_atomic(&legacy_auth, Some(provider_a_config))
.expect("seed existing codex live config");
let mut initial_config = MultiAppConfig::default();
{
let manager = initial_config
.get_manager_mut(&AppType::Codex)
.expect("codex manager");
manager.current = "provider-a".to_string();
manager.providers.insert(
"provider-a".to_string(),
Provider::with_id(
"provider-a".to_string(),
"RightCode".to_string(),
json!({
"auth": {"OPENAI_API_KEY": "rightcode-key"},
"config": provider_a_config
}),
None,
),
);
manager.providers.insert(
"provider-b".to_string(),
Provider::with_id(
"provider-b".to_string(),
"AiHubMix".to_string(),
json!({
"auth": {"OPENAI_API_KEY": "aihubmix-key"},
"config": r#"model_provider = "aihubmix"
model = "gpt-5.4"
profile = "work"
[model_providers.aihubmix]
name = "AiHubMix"
base_url = "https://aihubmix.example/v1"
wire_api = "responses"
requires_openai_auth = true
[profiles.work]
model_provider = "aihubmix"
model = "gpt-5.4"
"#
}),
None,
),
);
manager.providers.insert(
"provider-c".to_string(),
Provider::with_id(
"provider-c".to_string(),
"Vendor C".to_string(),
json!({
"auth": {"OPENAI_API_KEY": "vendor-c-key"},
"config": r#"model_provider = "vendor_c"
model = "gpt-5.4"
[model_providers.vendor_c]
name = "Vendor C"
base_url = "https://vendor-c.example/v1"
wire_api = "responses"
requires_openai_auth = true
"#
}),
None,
),
);
}
let state = create_test_state_with_config(&initial_config).expect("create test state");
ProviderService::switch(&state, AppType::Codex, "provider-b")
.expect("switch to provider b should succeed");
ProviderService::switch(&state, AppType::Codex, "provider-c")
.expect("switch to provider c should succeed");
let providers = state
.db
.get_all_providers(AppType::Codex.as_str())
.expect("read providers after switches");
let provider_b_config = providers
.get("provider-b")
.expect("provider b exists")
.settings_config
.get("config")
.and_then(|v| v.as_str())
.expect("provider b config");
let parsed: toml::Value = toml::from_str(provider_b_config).expect("parse provider b config");
assert_eq!(
parsed.get("model_provider").and_then(|v| v.as_str()),
Some("aihubmix"),
"backfill should restore provider b's storage-specific model_provider id"
);
assert!(
parsed
.get("model_providers")
.and_then(|v| v.get("aihubmix"))
.is_some(),
"provider b should keep its own model_providers table after backfill"
);
assert_eq!(
parsed
.get("profiles")
.and_then(|v| v.get("work"))
.and_then(|v| v.get("model_provider"))
.and_then(|v| v.as_str()),
Some("aihubmix"),
"profile overrides should be restored to provider b's storage-specific id"
);
}
#[test]
fn sync_current_provider_for_app_keeps_live_takeover_and_updates_restore_backup() {
let _guard = test_mutex().lock().expect("acquire test mutex");