mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 19:12:04 +08:00
fix(proxy): improve GitHub Copilot compatibility with modern Claude Code (#5832)
* 修复 Copilot 与新版 Claude Code 的兼容问题 * docs(proxy): correct Copilot placeholder rationale to the real mechanism Claude Code (verified on 2.1.220) does not format-validate ANTHROPIC_API_KEY against sk-ant-*: in headless mode the placeholder is sent upstream as-is. The actual failure mode is the interactive custom-API-key approval prompt, which defaults to "No (recommended)" — following the default ignores the key and lands users in "Not logged in". Also drop the #3289 citation, which describes a missing-placeholder scenario, not key validation. --------- Co-authored-by: Jason <farion1231@gmail.com>
This commit is contained in:
@@ -87,6 +87,17 @@ impl Provider {
|
|||||||
|| self.claude_base_url_contains("chatgpt.com/backend-api/codex")
|
|| self.claude_base_url_contains("chatgpt.com/backend-api/codex")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the provider form's "auth field" was explicitly set to
|
||||||
|
/// ANTHROPIC_API_KEY. The form only persists `meta.apiKeyField` for the
|
||||||
|
/// non-default choice, so `None` means the default ANTHROPIC_AUTH_TOKEN.
|
||||||
|
pub fn claude_uses_api_key_field(&self) -> bool {
|
||||||
|
self.meta
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|m| m.api_key_field.as_deref())
|
||||||
|
.map(|field| field.eq_ignore_ascii_case("ANTHROPIC_API_KEY"))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn provider_type(&self) -> Option<&str> {
|
fn provider_type(&self) -> Option<&str> {
|
||||||
self.meta.as_ref().and_then(|m| m.provider_type.as_deref())
|
self.meta.as_ref().and_then(|m| m.provider_type.as_deref())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1183,6 +1183,15 @@ impl RequestForwarder {
|
|||||||
super::providers::copilot_model_map::apply_copilot_model_normalization(mapped_body);
|
super::providers::copilot_model_map::apply_copilot_model_normalization(mapped_body);
|
||||||
self.apply_copilot_live_model_resolution(provider, &mut mapped_body)
|
self.apply_copilot_live_model_resolution(provider, &mut mapped_body)
|
||||||
.await;
|
.await;
|
||||||
|
// Strip the [1M] context marker after Copilot normalization/resolve.
|
||||||
|
// A user's mapped value (e.g. "gpt-5.6-sol[1M]") carries [1M] as a
|
||||||
|
// Claude Code context-capability declaration that upstream APIs reject
|
||||||
|
// as part of the model name. The preceding normalization step already
|
||||||
|
// rewrites claude-xxx[1M] into the "-1m" dash form Copilot accepts, and
|
||||||
|
// the strip helper only touches the "[1m]" bracket form, so "-1m"
|
||||||
|
// variants pass through unchanged.
|
||||||
|
mapped_body =
|
||||||
|
super::model_mapper::strip_one_m_suffix_for_upstream_from_body(mapped_body);
|
||||||
} else if !codex_responses_to_anthropic {
|
} else if !codex_responses_to_anthropic {
|
||||||
// Skip on the Codex→Anthropic path: stripping [1m] here would break both the
|
// Skip on the Codex→Anthropic path: stripping [1m] here would break both the
|
||||||
// model-catalog match (apply_codex_upstream_model) and the transform's own
|
// model-catalog match (apply_codex_upstream_model) and the transform's own
|
||||||
|
|||||||
@@ -95,9 +95,16 @@ impl ProxyService {
|
|||||||
let auth_policy = if provider.uses_managed_account_auth() {
|
let auth_policy = if provider.uses_managed_account_auth() {
|
||||||
// Codex 系(含仅凭 base_url 识别、无 provider_type meta 的)必须保留
|
// Codex 系(含仅凭 base_url 识别、无 provider_type meta 的)必须保留
|
||||||
// ANTHROPIC_AUTH_TOKEN 占位符:Claude Code 缺该键会弹登录提示(#3784)。
|
// ANTHROPIC_AUTH_TOKEN 占位符:Claude Code 缺该键会弹登录提示(#3784)。
|
||||||
// Copilot 维持仅 API_KEY 占位,避免与 /login 管理的 key 冲突(#1049)。
|
// Copilot 默认同样注入 AUTH_TOKEN 占位符:Claude Code(实测 2.1.220)
|
||||||
|
// 对 ANTHROPIC_API_KEY 会弹"是否使用该自定义 key"确认框且默认
|
||||||
|
// "No (recommended)",按默认走后占位符被忽略、落入 Not logged in
|
||||||
|
// (并非 sk-ant-* 格式校验——headless 下占位符原样出站);AUTH_TOKEN
|
||||||
|
// 作为网关 Bearer 被直接信任,零弹窗。仅当供应商表单显式选择了
|
||||||
|
// ANTHROPIC_API_KEY(meta.apiKeyField)时才保留 API_KEY 占位,以规避
|
||||||
|
// 与 /login 管理的 key 冲突(#1049)。
|
||||||
ClaudeTakeoverAuthPolicy::ManagedAccount {
|
ClaudeTakeoverAuthPolicy::ManagedAccount {
|
||||||
keep_auth_token: !provider.is_github_copilot(),
|
keep_auth_token: !provider.is_github_copilot()
|
||||||
|
|| !provider.claude_uses_api_key_field(),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken
|
ClaudeTakeoverAuthPolicy::PreserveExistingOrAuthToken
|
||||||
@@ -197,7 +204,10 @@ impl ProxyService {
|
|||||||
// - Codex 系保留 AUTH_TOKEN:缺该键 Claude Code 会弹登录提示(#3784)。
|
// - Codex 系保留 AUTH_TOKEN:缺该键 Claude Code 会弹登录提示(#3784)。
|
||||||
// 无条件注入而非"已存在才保留":热切换路径传入的是 provider
|
// 无条件注入而非"已存在才保留":热切换路径传入的是 provider
|
||||||
// settings(预设不含该键),且旧版接管已把存量用户 live 中的键删光。
|
// settings(预设不含该键),且旧版接管已把存量用户 live 中的键删光。
|
||||||
// - Copilot 仅 API_KEY:避免与 /login 管理的 key 冲突(#1049)。
|
// - Copilot 默认 AUTH_TOKEN:API_KEY 占位符会触发 Claude Code 的
|
||||||
|
// 自定义 key 确认框(默认 "No (recommended)"),按默认走即
|
||||||
|
// Not logged in;仅当表单显式选择了 ANTHROPIC_API_KEY 时才用
|
||||||
|
// API_KEY 占位以规避 /login key 冲突(#1049)。
|
||||||
if keep_auth_token {
|
if keep_auth_token {
|
||||||
env.insert(
|
env.insert(
|
||||||
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
"ANTHROPIC_AUTH_TOKEN".to_string(),
|
||||||
@@ -3304,7 +3314,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn managed_account_claude_takeover_uses_api_key_placeholder() {
|
fn managed_account_claude_takeover_uses_auth_token_placeholder() {
|
||||||
let mut provider = Provider::with_id(
|
let mut provider = Provider::with_id(
|
||||||
"copilot".to_string(),
|
"copilot".to_string(),
|
||||||
"GitHub Copilot".to_string(),
|
"GitHub Copilot".to_string(),
|
||||||
@@ -3333,13 +3343,13 @@ mod tests {
|
|||||||
.and_then(|value| value.as_object())
|
.and_then(|value| value.as_object())
|
||||||
.expect("env should exist");
|
.expect("env should exist");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
env.get("ANTHROPIC_API_KEY")
|
env.get("ANTHROPIC_AUTH_TOKEN")
|
||||||
.and_then(|value| value.as_str()),
|
.and_then(|value| value.as_str()),
|
||||||
Some(PROXY_TOKEN_PLACEHOLDER)
|
Some(PROXY_TOKEN_PLACEHOLDER)
|
||||||
);
|
);
|
||||||
assert!(
|
assert!(
|
||||||
env.get("ANTHROPIC_AUTH_TOKEN").is_none(),
|
env.get("ANTHROPIC_API_KEY").is_none(),
|
||||||
"managed OAuth providers should avoid Claude Auth Token login semantics"
|
"API_KEY placeholders trigger Claude Code's custom-key approval prompt (defaults to No), landing users in Not logged in"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3421,8 +3431,8 @@ mod tests {
|
|||||||
"CLAUDE_CODE_SUBAGENT_MODEL",
|
"CLAUDE_CODE_SUBAGENT_MODEL",
|
||||||
Some("claude-sonnet-4.6[1M]"),
|
Some("claude-sonnet-4.6[1M]"),
|
||||||
);
|
);
|
||||||
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
|
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER));
|
||||||
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
|
assert_env_str(env, "ANTHROPIC_API_KEY", None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -3675,7 +3685,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn managed_account_claude_takeover_copilot_removes_stale_auth_token() {
|
fn managed_account_claude_takeover_copilot_defaults_to_auth_token() {
|
||||||
let mut provider = Provider::with_id(
|
let mut provider = Provider::with_id(
|
||||||
"copilot".to_string(),
|
"copilot".to_string(),
|
||||||
"GitHub Copilot".to_string(),
|
"GitHub Copilot".to_string(),
|
||||||
@@ -3691,6 +3701,48 @@ mod tests {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut live_config = json!({
|
||||||
|
"env": {
|
||||||
|
"ANTHROPIC_BASE_URL": "https://stale.example.com",
|
||||||
|
"ANTHROPIC_AUTH_TOKEN": "stale-token",
|
||||||
|
"ANTHROPIC_API_KEY": "stale-key"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
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");
|
||||||
|
// Default Copilot takeover injects AUTH_TOKEN: the API_KEY placeholder
|
||||||
|
// triggers Claude Code's custom-key approval prompt (defaults to
|
||||||
|
// "No (recommended)"), which lands users in "Not logged in".
|
||||||
|
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", Some(PROXY_TOKEN_PLACEHOLDER));
|
||||||
|
assert_env_str(env, "ANTHROPIC_API_KEY", None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn managed_account_claude_takeover_copilot_honors_api_key_field_choice() {
|
||||||
|
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()),
|
||||||
|
api_key_field: Some("ANTHROPIC_API_KEY".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
let mut live_config = json!({
|
let mut live_config = json!({
|
||||||
"env": {
|
"env": {
|
||||||
"ANTHROPIC_BASE_URL": "https://stale.example.com",
|
"ANTHROPIC_BASE_URL": "https://stale.example.com",
|
||||||
@@ -3707,6 +3759,8 @@ mod tests {
|
|||||||
.get("env")
|
.get("env")
|
||||||
.and_then(|value| value.as_object())
|
.and_then(|value| value.as_object())
|
||||||
.expect("env should exist");
|
.expect("env should exist");
|
||||||
|
// Explicit API-key-field choice keeps the API_KEY placeholder to avoid
|
||||||
|
// conflicting with the /login-managed key (#1049).
|
||||||
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
|
assert_env_str(env, "ANTHROPIC_API_KEY", Some(PROXY_TOKEN_PLACEHOLDER));
|
||||||
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
|
assert_env_str(env, "ANTHROPIC_AUTH_TOKEN", None);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user