mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
修复 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:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user