mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-08-03 02:51:17 +08:00
Merge branch 'main' into feat/proxy-full-url
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
//! - **Claude**: Anthropic 官方 API (x-api-key + anthropic-version)
|
||||
//! - **ClaudeAuth**: 中转服务 (仅 Bearer 认证,无 x-api-key)
|
||||
//! - **OpenRouter**: 已支持 Claude Code 兼容接口,默认透传
|
||||
//! - **GitHubCopilot**: GitHub Copilot (OAuth + Copilot Token)
|
||||
|
||||
use super::{AuthInfo, AuthStrategy, ProviderAdapter, ProviderType};
|
||||
use crate::provider::Provider;
|
||||
@@ -76,10 +77,16 @@ impl ClaudeAdapter {
|
||||
/// 获取供应商类型
|
||||
///
|
||||
/// 根据 base_url 和 auth_mode 检测具体的供应商类型:
|
||||
/// - GitHubCopilot: meta.provider_type 为 github_copilot 或 base_url 包含 githubcopilot.com
|
||||
/// - OpenRouter: base_url 包含 openrouter.ai
|
||||
/// - ClaudeAuth: auth_mode 为 bearer_only
|
||||
/// - Claude: 默认 Anthropic 官方
|
||||
pub fn provider_type(&self, provider: &Provider) -> ProviderType {
|
||||
// 检测 GitHub Copilot
|
||||
if self.is_github_copilot(provider) {
|
||||
return ProviderType::GitHubCopilot;
|
||||
}
|
||||
|
||||
// 检测 OpenRouter
|
||||
if self.is_openrouter(provider) {
|
||||
return ProviderType::OpenRouter;
|
||||
@@ -93,6 +100,25 @@ impl ClaudeAdapter {
|
||||
ProviderType::Claude
|
||||
}
|
||||
|
||||
/// 检测是否为 GitHub Copilot 供应商
|
||||
fn is_github_copilot(&self, provider: &Provider) -> bool {
|
||||
// 方式1: 检查 meta.provider_type
|
||||
if let Some(meta) = provider.meta.as_ref() {
|
||||
if meta.provider_type.as_deref() == Some("github_copilot") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// 方式2: 检查 base_url(兼容旧数据的 fallback,后续应优先依赖 providerType)
|
||||
if let Ok(base_url) = self.extract_base_url(provider) {
|
||||
if base_url.contains("githubcopilot.com") {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// 检测是否使用 OpenRouter
|
||||
fn is_openrouter(&self, provider: &Provider) -> bool {
|
||||
if let Ok(base_url) = self.extract_base_url(provider) {
|
||||
@@ -244,6 +270,17 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
|
||||
fn extract_auth(&self, provider: &Provider) -> Option<AuthInfo> {
|
||||
let provider_type = self.provider_type(provider);
|
||||
|
||||
// GitHub Copilot 使用特殊的认证策略
|
||||
// 实际的 token 会在代理请求时动态获取
|
||||
if provider_type == ProviderType::GitHubCopilot {
|
||||
// 返回一个占位符,实际 token 由 CopilotAuthManager 动态提供
|
||||
return Some(AuthInfo::new(
|
||||
"copilot_placeholder".to_string(),
|
||||
AuthStrategy::GitHubCopilot,
|
||||
));
|
||||
}
|
||||
|
||||
let strategy = match provider_type {
|
||||
ProviderType::OpenRouter => AuthStrategy::Bearer,
|
||||
ProviderType::ClaudeAuth => AuthStrategy::ClaudeAuth,
|
||||
@@ -275,7 +312,24 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
base = base.replace("/v1/v1", "/v1");
|
||||
}
|
||||
|
||||
base
|
||||
// GitHub Copilot 不需要 ?beta=true 参数
|
||||
if base_url.contains("githubcopilot.com") {
|
||||
return base;
|
||||
}
|
||||
|
||||
// 为 Claude 原生 /v1/messages 端点添加 ?beta=true 参数
|
||||
// 这是某些上游服务(如 DuckCoding)验证请求来源的关键参数
|
||||
// 注意:不要为 OpenAI Chat Completions (/v1/chat/completions) 添加此参数
|
||||
// 当 apiFormat="openai_chat" 时,请求会转发到 /v1/chat/completions,
|
||||
// 但该端点是 OpenAI 标准,不支持 ?beta=true 参数
|
||||
if endpoint.contains("/v1/messages")
|
||||
&& !endpoint.contains("/v1/chat/completions")
|
||||
&& !endpoint.contains('?')
|
||||
{
|
||||
format!("{base}?beta=true")
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
fn add_auth_headers(&self, request: RequestBuilder, auth: &AuthInfo) -> RequestBuilder {
|
||||
@@ -294,11 +348,22 @@ impl ProviderAdapter for ClaudeAdapter {
|
||||
AuthStrategy::Bearer => {
|
||||
request.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||
}
|
||||
// GitHub Copilot: Bearer + 特定的 Editor headers
|
||||
AuthStrategy::GitHubCopilot => request
|
||||
.header("Authorization", format!("Bearer {}", auth.api_key))
|
||||
.header("Editor-Version", "vscode/1.85.0")
|
||||
.header("Editor-Plugin-Version", "copilot/1.150.0")
|
||||
.header("Copilot-Integration-Id", "vscode-chat"),
|
||||
_ => request,
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_transform(&self, provider: &Provider) -> bool {
|
||||
// GitHub Copilot 总是需要格式转换 (Anthropic → OpenAI)
|
||||
if self.is_github_copilot(provider) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 根据 api_format 配置决定是否需要格式转换
|
||||
// - "anthropic" (默认): 直接透传,无需转换
|
||||
// - "openai_chat": 需要 Anthropic ↔ OpenAI Chat Completions 格式转换
|
||||
@@ -513,14 +578,14 @@ mod tests {
|
||||
fn test_build_url_anthropic() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_openrouter() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let url = adapter.build_url("https://openrouter.ai/api", "/v1/messages");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages");
|
||||
assert_eq!(url, "https://openrouter.ai/api/v1/messages?beta=true");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -533,8 +598,15 @@ mod tests {
|
||||
#[test]
|
||||
fn test_build_url_preserve_existing_query() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages?beta=true");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?beta=true");
|
||||
let url = adapter.build_url("https://api.anthropic.com", "/v1/messages?foo=bar");
|
||||
assert_eq!(url, "https://api.anthropic.com/v1/messages?foo=bar");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_url_no_beta_for_github_copilot() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
let url = adapter.build_url("https://api.githubcopilot.com", "/v1/messages");
|
||||
assert_eq!(url, "https://api.githubcopilot.com/v1/messages");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -664,4 +736,67 @@ mod tests {
|
||||
);
|
||||
assert!(!adapter.needs_transform(&unknown_format));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_github_copilot_detection_by_url() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
// GitHub Copilot by base_url
|
||||
let copilot = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com"
|
||||
}
|
||||
}));
|
||||
assert_eq!(adapter.provider_type(&copilot), ProviderType::GitHubCopilot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_github_copilot_detection_by_meta() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
// GitHub Copilot by meta.provider_type
|
||||
let copilot_meta = create_provider_with_meta(
|
||||
json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.example.com"
|
||||
}
|
||||
}),
|
||||
ProviderMeta {
|
||||
provider_type: Some("github_copilot".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
assert_eq!(
|
||||
adapter.provider_type(&copilot_meta),
|
||||
ProviderType::GitHubCopilot
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_github_copilot_auth() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
let copilot = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com"
|
||||
}
|
||||
}));
|
||||
|
||||
let auth = adapter.extract_auth(&copilot).unwrap();
|
||||
assert_eq!(auth.strategy, AuthStrategy::GitHubCopilot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_github_copilot_needs_transform() {
|
||||
let adapter = ClaudeAdapter::new();
|
||||
|
||||
let copilot = create_provider(json!({
|
||||
"env": {
|
||||
"ANTHROPIC_BASE_URL": "https://api.githubcopilot.com"
|
||||
}
|
||||
}));
|
||||
|
||||
// GitHub Copilot always needs transform
|
||||
assert!(adapter.needs_transform(&copilot));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user