fix(proxy): source takeover model fields from target provider on managed accounts

When a managed-account provider (GitHub Copilot or Codex OAuth) is taken
over by the local proxy, the *_MODEL_NAME entries written to Claude live
config used to be snapshotted from the live file on disk, which still
held the *previous* provider's values during a switch. The result was a
stale display name lingering after the switch.

Read the model snapshot from the target provider's effective settings
(common-config merged) when the provider uses managed-account auth;
preserve the existing behavior of reading from the live config for
normal providers. The three takeover entry points now resolve the
effective provider up front so common-config writes flow through.
This commit is contained in:
Jason
2026-05-23 09:43:21 +08:00
parent 014c82d28b
commit e25682d345
+191 -6
View File
@@ -94,8 +94,19 @@ impl ProxyService {
} else {
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken
};
// Copilot/Codex 接管时 live config 可能还是旧供应商;显示模型必须跟随目标 provider。
let takeover_model_fields = if provider.uses_managed_account_auth() {
Self::build_claude_takeover_model_fields(&provider.settings_config)
} else {
Self::build_claude_takeover_model_fields(config)
};
Self::apply_claude_takeover_fields_with_policy(config, proxy_url, auth_policy);
Self::apply_claude_takeover_fields_with_policy_and_models(
config,
proxy_url,
auth_policy,
takeover_model_fields,
);
}
fn apply_claude_takeover_fields_with_policy(
@@ -106,6 +117,20 @@ impl ProxyService {
// 必须在 remove/insert 前 snapshot:避免读到自己刚写入的接管别名。
let takeover_model_fields = Self::build_claude_takeover_model_fields(config);
Self::apply_claude_takeover_fields_with_policy_and_models(
config,
proxy_url,
auth_policy,
takeover_model_fields,
);
}
fn apply_claude_takeover_fields_with_policy_and_models(
config: &mut Value,
proxy_url: &str,
auth_policy: ClaudeTakeoverAuthPolicy,
takeover_model_fields: Vec<(&'static str, String)>,
) {
if !config.is_object() {
*config = json!({});
}
@@ -262,22 +287,32 @@ impl ProxyService {
.to_string()
}
pub async fn sync_claude_live_from_provider_while_proxy_active(
fn claude_provider_with_effective_settings(
&self,
provider: &Provider,
) -> Result<(), String> {
let mut effective_settings = build_effective_settings_with_common_config(
) -> Result<Provider, String> {
let mut effective_provider = provider.clone();
effective_provider.settings_config = build_effective_settings_with_common_config(
self.db.as_ref(),
&AppType::Claude,
provider,
)
.map_err(|e| format!("构建 claude 有效配置失败: {e}"))?;
Ok(effective_provider)
}
pub async fn sync_claude_live_from_provider_while_proxy_active(
&self,
provider: &Provider,
) -> Result<(), String> {
let effective_provider = self.claude_provider_with_effective_settings(provider)?;
let mut effective_settings = effective_provider.settings_config.clone();
let (proxy_url, _) = self.build_proxy_urls().await?;
Self::apply_claude_takeover_fields_for_provider(
&mut effective_settings,
&proxy_url,
provider,
&effective_provider,
);
self.write_claude_live(&effective_settings)?;
Ok(())
@@ -1118,6 +1153,7 @@ impl ProxyService {
// Claude: 修改 ANTHROPIC_BASE_URL,使用占位符替代真实 Token(代理会注入真实 Token
if let Ok(mut live_config) = self.read_claude_live() {
let claude_provider = self.require_current_provider_for_app(&AppType::Claude)?;
let claude_provider = self.claude_provider_with_effective_settings(&claude_provider)?;
Self::apply_claude_takeover_fields_for_provider(
&mut live_config,
&proxy_url,
@@ -1185,6 +1221,8 @@ impl ProxyService {
AppType::Claude => {
let mut live_config = self.read_claude_live()?;
let claude_provider = self.require_current_provider_for_app(&AppType::Claude)?;
let claude_provider =
self.claude_provider_with_effective_settings(&claude_provider)?;
Self::apply_claude_takeover_fields_for_provider(
&mut live_config,
&proxy_url,
@@ -1256,10 +1294,11 @@ impl ProxyService {
.ok()
.flatten();
if let Some(provider) = claude_provider.as_ref() {
let provider = self.claude_provider_with_effective_settings(provider)?;
Self::apply_claude_takeover_fields_for_provider(
&mut live_config,
&proxy_url,
provider,
&provider,
);
} else {
Self::apply_claude_takeover_fields_with_policy(
@@ -2295,6 +2334,10 @@ mod tests {
}
}
fn assert_env_str(env: &Map<String, Value>, key: &str, expected: Option<&str>) {
assert_eq!(env.get(key).and_then(|value| value.as_str()), expected);
}
#[test]
fn managed_account_claude_takeover_uses_api_key_placeholder() {
let mut provider = Provider::with_id(
@@ -2335,6 +2378,148 @@ mod tests {
);
}
#[test]
fn managed_account_claude_takeover_sources_copilot_models_from_provider() {
let mut provider = Provider::with_id(
"copilot".to_string(),
"GitHub Copilot".to_string(),
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com",
"ANTHROPIC_MODEL": "claude-sonnet-4.6",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4.5",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4.6",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-sonnet-4.6"
}
}),
None,
);
provider.meta = Some(ProviderMeta {
provider_type: Some("github_copilot".to_string()),
..Default::default()
});
let mut live_config = json!({
"env": {
"ANTHROPIC_BASE_URL": "https://stale.example.com",
"ANTHROPIC_API_KEY": "stale-key",
"ANTHROPIC_MODEL": "stale-model",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "stale-haiku",
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "Stale Haiku",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "stale-sonnet",
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "stale-opus",
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Stale Opus"
}
});
ProxyService::apply_claude_takeover_fields_for_provider(
&mut live_config,
"http://127.0.0.1:15721",
&provider,
);
let env = live_config
.get("env")
.and_then(|value| value.as_object())
.expect("env should exist");
assert_env_str(env, "ANTHROPIC_MODEL", None);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
Some("claude-haiku-4-5"),
);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
Some("claude-haiku-4.5"),
);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_SONNET_MODEL",
Some("claude-sonnet-4-6"),
);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME",
Some("claude-sonnet-4.6"),
);
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-7"));
assert_env_str(
env,
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME",
Some("claude-sonnet-4.6"),
);
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
}
#[test]
fn managed_account_claude_takeover_sources_codex_models_from_provider() {
let mut provider = Provider::with_id(
"codex".to_string(),
"Codex".to_string(),
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex",
"ANTHROPIC_MODEL": "gpt-5.4",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "gpt-5.4-mini",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "gpt-5.4",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "gpt-5.4"
}
}),
None,
);
provider.meta = Some(ProviderMeta {
provider_type: Some("codex_oauth".to_string()),
..Default::default()
});
let mut live_config = json!({
"env": {
"ANTHROPIC_BASE_URL": "https://stale.example.com",
"ANTHROPIC_AUTH_TOKEN": "stale-token",
"ANTHROPIC_MODEL": "stale-model",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "stale-haiku",
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME": "Stale Haiku",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "stale-sonnet",
"ANTHROPIC_DEFAULT_SONNET_MODEL_NAME": "Stale Sonnet",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "stale-opus",
"ANTHROPIC_DEFAULT_OPUS_MODEL_NAME": "Stale Opus"
}
});
ProxyService::apply_claude_takeover_fields_for_provider(
&mut live_config,
"http://127.0.0.1:15721",
&provider,
);
let env = live_config
.get("env")
.and_then(|value| value.as_object())
.expect("env should exist");
assert_env_str(env, "ANTHROPIC_MODEL", None);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_HAIKU_MODEL",
Some("claude-haiku-4-5"),
);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME",
Some("gpt-5.4-mini"),
);
assert_env_str(
env,
"ANTHROPIC_DEFAULT_SONNET_MODEL",
Some("claude-sonnet-4-6"),
);
assert_env_str(env, "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", Some("gpt-5.4"));
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-7"));
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", Some("gpt-5.4"));
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
}
#[test]
fn normal_claude_takeover_without_token_keeps_auth_token_fallback() {
let mut live_config = json!({