fix(proxy): preserve Codex OAuth auth token on takeover (#3789)

* fix(proxy): preserve Codex OAuth auth token on takeover

* style(proxy): format Codex OAuth takeover fix

* fix(proxy): unconditionally inject AUTH_TOKEN placeholder for codex takeover

The preserve-if-exists condition left #3784 unfixed on three paths:
hot-switch passes the provider's settings (presets carry no
ANTHROPIC_AUTH_TOKEN key), fresh installs never had the key, and live
configs already stripped by older releases stay stripped.

- Fold the bool parameter into the policy enum as
  ManagedAccount { keep_auth_token } so every construction site
  declares intent
- Decide via !is_github_copilot() within the managed branch so
  URL-only codex providers (no provider_type meta) are covered,
  matching the predicate family used for policy selection
- Inject the placeholder unconditionally instead of only when the
  key pre-exists; Copilot behavior is unchanged (API_KEY only)
- Pin the previously uncovered cases with tests: codex without a
  pre-existing key, URL-only codex, and Copilot removing a stale
  AUTH_TOKEN

---------

Co-authored-by: codeasier <liuyekang@huawei.com>
Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
codeasier
2026-06-11 22:21:42 +08:00
committed by GitHub
Unverified
parent d70e3828fe
commit 2d64d8c619
+118 -3
View File
@@ -48,7 +48,7 @@ const CLAUDE_ONE_M_MARKER_FOR_CLIENT: &str = "[1M]";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ClaudeTakeoverAuthPolicy {
PreserveExistingOrAuthToken,
ManagedAccount,
ManagedAccount { keep_auth_token: bool },
}
#[derive(Clone)]
@@ -90,7 +90,12 @@ impl ProxyService {
provider: &Provider,
) {
let auth_policy = if provider.uses_managed_account_auth() {
ClaudeTakeoverAuthPolicy::ManagedAccount
// Codex 系(含仅凭 base_url 识别、无 provider_type meta 的)必须保留
// ANTHROPIC_AUTH_TOKEN 占位符:Claude Code 缺该键会弹登录提示(#3784)。
// Copilot 维持仅 API_KEY 占位,避免与 /login 管理的 key 冲突(#1049)。
ClaudeTakeoverAuthPolicy::ManagedAccount {
keep_auth_token: !provider.is_github_copilot(),
}
} else {
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken
};
@@ -180,7 +185,7 @@ impl ProxyService {
);
}
}
ClaudeTakeoverAuthPolicy::ManagedAccount => {
ClaudeTakeoverAuthPolicy::ManagedAccount { keep_auth_token } => {
for key in token_keys {
env.remove(key);
}
@@ -188,6 +193,14 @@ impl ProxyService {
"ANTHROPIC_API_KEY".to_string(),
json!(PROXY_TOKEN_PLACEHOLDER),
);
if keep_auth_token {
// 无条件注入而非"已存在才保留":热切换路径传入的是 provider
// settings(预设不含该键),且旧版接管已把存量用户 live 中的键删光。
env.insert(
"ANTHROPIC_AUTH_TOKEN".to_string(),
json!(PROXY_TOKEN_PLACEHOLDER),
);
}
}
}
}
@@ -2945,6 +2958,108 @@ mod tests {
assert_env_str(env, "ANTHROPIC_DEFAULT_OPUS_MODEL", Some("claude-opus-4-8"));
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", Some(PROXY_TOKEN_PLACEHOLDER));
}
#[test]
fn managed_account_claude_takeover_codex_injects_auth_token_without_preexisting_key() {
let mut provider = Provider::with_id(
"codex".to_string(),
"Codex".to_string(),
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex"
}
}),
None,
);
provider.meta = Some(ProviderMeta {
provider_type: Some("codex_oauth".to_string()),
..Default::default()
});
// 全新安装/热切换形态:传入的 env 没有任何 token 键。
let mut live_config = provider.settings_config.clone();
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_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER));
}
#[test]
fn managed_account_claude_takeover_codex_by_base_url_keeps_auth_token() {
// 无 provider_type meta、仅凭 base_url 识别为受管 codex 的供应商,
// 也必须保留 AUTH_TOKEN 占位符(与策略选择共用同一判定族)。
let provider = Provider::with_id(
"codex-url-only".to_string(),
"Codex (URL only)".to_string(),
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://chatgpt.com/backend-api/codex"
}
}),
None,
);
assert!(provider.uses_managed_account_auth());
assert!(!provider.is_codex_oauth());
let mut live_config = provider.settings_config.clone();
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_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER));
}
#[test]
fn managed_account_claude_takeover_copilot_removes_stale_auth_token() {
let mut provider = Provider::with_id(
"copilot".to_string(),
"GitHub Copilot".to_string(),
json!({
"env": {
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com"
}
}),
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_AUTH_TOKEN": "stale-token"
}
});
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_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
}