//! Provider Adapters Module //! //! 供应商适配器模块,提供统一的接口抽象不同上游供应商的处理逻辑。 //! //! ## 模块结构 //! - `adapter`: 定义 `ProviderAdapter` trait //! - `auth`: 认证类型和策略 //! - `claude`: Claude (Anthropic) 适配器 //! - `codex`: Codex (OpenAI) 适配器 //! - `gemini`: Gemini (Google) 适配器 //! - `models`: API 数据模型 //! - `transform`: 格式转换 mod adapter; mod auth; mod claude; mod codex; pub(crate) mod codex_chat_common; pub mod codex_chat_history; pub mod codex_oauth_auth; pub mod copilot_auth; pub mod copilot_model_map; mod gemini; pub(crate) mod gemini_schema; pub mod gemini_shadow; pub mod models; pub mod streaming; pub mod streaming_codex_chat; pub mod streaming_gemini; pub mod streaming_responses; pub mod transform; pub mod transform_codex_chat; pub mod transform_gemini; pub mod transform_responses; use crate::app_config::AppType; use crate::provider::Provider; use serde::{Deserialize, Serialize}; // 公开导出 pub use adapter::ProviderAdapter; pub use auth::{AuthInfo, AuthStrategy}; pub use claude::{ claude_api_format_needs_transform, get_claude_api_format, normalize_anthropic_messages_for_provider, transform_claude_request_for_api_format, ClaudeAdapter, }; pub use codex::CodexAdapter; pub use codex::{ apply_codex_chat_upstream_model, codex_provider_upstream_model, codex_provider_uses_chat_completions, is_origin_only_url, resolve_codex_chat_reasoning_config, should_convert_codex_responses_to_chat, }; pub use gemini::GeminiAdapter; /// 供应商类型枚举 /// /// 区分不同供应商的具体实现方式,决定认证和请求处理逻辑。 /// 比 AppType 更细粒度,支持同一 AppType 下的多种变体。 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ProviderType { /// Anthropic 官方 API (x-api-key + anthropic-version) Claude, /// Claude 中转服务 (仅 Bearer 认证,无 x-api-key) ClaudeAuth, /// OpenAI Codex Response API Codex, /// Google Gemini API (x-goog-api-key) Gemini, /// Google Gemini CLI (OAuth Bearer) GeminiCli, /// OpenRouter(已支持 Claude Code 兼容接口,默认透传;保留旧转换逻辑备用) OpenRouter, /// GitHub Copilot (OAuth + Copilot Token,需要 Anthropic ↔ OpenAI 转换) GitHubCopilot, /// OpenAI Codex (ChatGPT Plus/Pro OAuth,需要 Anthropic ↔ Responses API 转换) CodexOAuth, } impl ProviderType { /// 是否需要格式转换 /// /// 过去 OpenRouter 需要将 Anthropic 格式转换为 OpenAI 格式; /// 现在默认关闭转换(因为 OpenRouter 已支持 Claude Code 兼容接口)。 /// GitHub Copilot 需要转换(Anthropic → OpenAI 格式)。 #[allow(dead_code)] pub fn needs_transform(&self) -> bool { match self { ProviderType::GitHubCopilot => true, ProviderType::CodexOAuth => true, ProviderType::OpenRouter => false, _ => false, } } /// 获取默认端点 #[allow(dead_code)] pub fn default_endpoint(&self) -> &'static str { match self { ProviderType::Claude | ProviderType::ClaudeAuth => "https://api.anthropic.com", ProviderType::Codex => "https://api.openai.com", ProviderType::Gemini | ProviderType::GeminiCli => { "https://generativelanguage.googleapis.com" } ProviderType::OpenRouter => "https://openrouter.ai/api", ProviderType::GitHubCopilot => "https://api.githubcopilot.com", ProviderType::CodexOAuth => "https://chatgpt.com/backend-api/codex", } } /// 从 AppType 和 Provider 配置推断供应商类型 /// /// 根据配置中的 base_url、auth_mode、api_key 格式等信息推断具体的供应商类型 #[allow(dead_code)] pub fn from_app_type_and_config(app_type: &AppType, provider: &Provider) -> Self { match app_type { AppType::Claude | AppType::ClaudeDesktop => { if get_claude_api_format(provider) == "gemini_native" { let adapter = ClaudeAdapter::new(); return match adapter.extract_auth(provider).map(|auth| auth.strategy) { Some(AuthStrategy::GoogleOAuth) => ProviderType::GeminiCli, _ => ProviderType::Gemini, }; } // 检测是否为 GitHub Copilot if let Some(meta) = provider.meta.as_ref() { if meta.provider_type.as_deref() == Some("github_copilot") { return ProviderType::GitHubCopilot; } if meta.provider_type.as_deref() == Some("codex_oauth") { return ProviderType::CodexOAuth; } } // 检测 base_url 是否为 GitHub Copilot let adapter = ClaudeAdapter::new(); if let Ok(base_url) = adapter.extract_base_url(provider) { if base_url.contains("githubcopilot.com") { return ProviderType::GitHubCopilot; } // 检测是否为 OpenRouter if base_url.contains("openrouter.ai") { return ProviderType::OpenRouter; } } // 检测是否为中转服务(仅 Bearer 认证) // 注意:ProviderMeta 没有直接的 auth_mode 字段, // 我们通过检查 settings_config 中的配置来判断 // 检查 settings_config 中的 auth_mode if let Some(auth_mode) = provider .settings_config .get("auth_mode") .and_then(|v| v.as_str()) { if auth_mode == "bearer_only" { return ProviderType::ClaudeAuth; } } // 检查 env 中的 auth_mode if let Some(env) = provider.settings_config.get("env") { if let Some(auth_mode) = env.get("AUTH_MODE").and_then(|v| v.as_str()) { if auth_mode == "bearer_only" { return ProviderType::ClaudeAuth; } } } ProviderType::Claude } AppType::Codex => ProviderType::Codex, AppType::Gemini => { // 检测是否为 CLI 模式(OAuth) let adapter = GeminiAdapter::new(); if let Some(auth) = adapter.extract_auth(provider) { let key = &auth.api_key; // OAuth access_token 以 ya29. 开头 if key.starts_with("ya29.") { return ProviderType::GeminiCli; } // JSON 格式的 OAuth 凭证 if key.starts_with('{') { return ProviderType::GeminiCli; } } ProviderType::Gemini } AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => { // These apps don't support proxy, fallback to Codex-like type ProviderType::Codex } } } /// 转换为字符串表示 pub fn as_str(&self) -> &'static str { match self { ProviderType::Claude => "claude", ProviderType::ClaudeAuth => "claude_auth", ProviderType::Codex => "codex", ProviderType::Gemini => "gemini", ProviderType::GeminiCli => "gemini_cli", ProviderType::OpenRouter => "openrouter", ProviderType::GitHubCopilot => "github_copilot", ProviderType::CodexOAuth => "codex_oauth", } } } impl std::fmt::Display for ProviderType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.as_str()) } } impl std::str::FromStr for ProviderType { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "claude" => Ok(ProviderType::Claude), "claude_auth" | "claude-auth" => Ok(ProviderType::ClaudeAuth), "codex" => Ok(ProviderType::Codex), "gemini" => Ok(ProviderType::Gemini), "gemini_cli" | "gemini-cli" => Ok(ProviderType::GeminiCli), "openrouter" => Ok(ProviderType::OpenRouter), "github_copilot" | "github-copilot" | "githubcopilot" => { Ok(ProviderType::GitHubCopilot) } "codex_oauth" | "codex-oauth" | "codexoauth" => Ok(ProviderType::CodexOAuth), _ => Err(format!("Invalid provider type: {s}")), } } } /// 根据 AppType 获取对应的适配器 pub fn get_adapter(app_type: &AppType) -> Box { match app_type { AppType::Claude | AppType::ClaudeDesktop => Box::new(ClaudeAdapter::new()), AppType::Codex => Box::new(CodexAdapter::new()), AppType::Gemini => Box::new(GeminiAdapter::new()), AppType::OpenCode | AppType::OpenClaw | AppType::Hermes => { // These apps don't support proxy, fallback to Codex adapter Box::new(CodexAdapter::new()) } } } /// 根据 ProviderType 获取对应的适配器 #[allow(dead_code)] pub fn get_adapter_for_provider_type(provider_type: &ProviderType) -> Box { match provider_type { ProviderType::Claude | ProviderType::ClaudeAuth | ProviderType::OpenRouter | ProviderType::GitHubCopilot | ProviderType::CodexOAuth => Box::new(ClaudeAdapter::new()), ProviderType::Codex => Box::new(CodexAdapter::new()), ProviderType::Gemini | ProviderType::GeminiCli => Box::new(GeminiAdapter::new()), } } #[cfg(test)] mod tests { use super::*; use serde_json::json; fn create_provider(config: serde_json::Value) -> Provider { Provider { id: "test".to_string(), name: "Test Provider".to_string(), settings_config: config, website_url: None, category: None, created_at: None, sort_index: None, notes: None, meta: None, icon: None, icon_color: None, in_failover_queue: false, } } #[test] fn test_provider_type_needs_transform() { assert!(!ProviderType::Claude.needs_transform()); assert!(!ProviderType::ClaudeAuth.needs_transform()); assert!(!ProviderType::Codex.needs_transform()); assert!(!ProviderType::Gemini.needs_transform()); assert!(!ProviderType::GeminiCli.needs_transform()); assert!(!ProviderType::OpenRouter.needs_transform()); assert!(ProviderType::GitHubCopilot.needs_transform()); } #[test] fn test_provider_type_default_endpoint() { assert_eq!( ProviderType::Claude.default_endpoint(), "https://api.anthropic.com" ); assert_eq!( ProviderType::ClaudeAuth.default_endpoint(), "https://api.anthropic.com" ); assert_eq!( ProviderType::Codex.default_endpoint(), "https://api.openai.com" ); assert_eq!( ProviderType::Gemini.default_endpoint(), "https://generativelanguage.googleapis.com" ); assert_eq!( ProviderType::GeminiCli.default_endpoint(), "https://generativelanguage.googleapis.com" ); assert_eq!( ProviderType::OpenRouter.default_endpoint(), "https://openrouter.ai/api" ); assert_eq!( ProviderType::GitHubCopilot.default_endpoint(), "https://api.githubcopilot.com" ); } #[test] fn test_provider_type_from_str() { assert_eq!( "claude".parse::().unwrap(), ProviderType::Claude ); assert_eq!( "claude_auth".parse::().unwrap(), ProviderType::ClaudeAuth ); assert_eq!( "claude-auth".parse::().unwrap(), ProviderType::ClaudeAuth ); assert_eq!( "codex".parse::().unwrap(), ProviderType::Codex ); assert_eq!( "gemini".parse::().unwrap(), ProviderType::Gemini ); assert_eq!( "gemini_cli".parse::().unwrap(), ProviderType::GeminiCli ); assert_eq!( "gemini-cli".parse::().unwrap(), ProviderType::GeminiCli ); assert_eq!( "openrouter".parse::().unwrap(), ProviderType::OpenRouter ); assert_eq!( "github_copilot".parse::().unwrap(), ProviderType::GitHubCopilot ); assert_eq!( "github-copilot".parse::().unwrap(), ProviderType::GitHubCopilot ); assert_eq!( "githubcopilot".parse::().unwrap(), ProviderType::GitHubCopilot ); assert!("invalid".parse::().is_err()); } #[test] fn test_provider_type_as_str() { assert_eq!(ProviderType::Claude.as_str(), "claude"); assert_eq!(ProviderType::ClaudeAuth.as_str(), "claude_auth"); assert_eq!(ProviderType::Codex.as_str(), "codex"); assert_eq!(ProviderType::Gemini.as_str(), "gemini"); assert_eq!(ProviderType::GeminiCli.as_str(), "gemini_cli"); assert_eq!(ProviderType::OpenRouter.as_str(), "openrouter"); assert_eq!(ProviderType::GitHubCopilot.as_str(), "github_copilot"); } #[test] fn test_provider_type_serde() { // Test serialization let claude = ProviderType::Claude; let serialized = serde_json::to_string(&claude).unwrap(); assert_eq!(serialized, "\"claude\""); let claude_auth = ProviderType::ClaudeAuth; let serialized = serde_json::to_string(&claude_auth).unwrap(); assert_eq!(serialized, "\"claude_auth\""); // Test deserialization let deserialized: ProviderType = serde_json::from_str("\"claude\"").unwrap(); assert_eq!(deserialized, ProviderType::Claude); let deserialized: ProviderType = serde_json::from_str("\"gemini_cli\"").unwrap(); assert_eq!(deserialized, ProviderType::GeminiCli); } #[test] fn test_from_app_type_claude_direct() { let provider = create_provider(json!({ "env": { "ANTHROPIC_BASE_URL": "https://api.anthropic.com", "ANTHROPIC_AUTH_TOKEN": "sk-ant-test" } })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Claude, &provider); assert_eq!(provider_type, ProviderType::Claude); } #[test] fn test_from_app_type_claude_openrouter() { let provider = create_provider(json!({ "env": { "ANTHROPIC_BASE_URL": "https://openrouter.ai/api", "OPENROUTER_API_KEY": "sk-or-test" } })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Claude, &provider); assert_eq!(provider_type, ProviderType::OpenRouter); } #[test] fn test_from_app_type_claude_auth() { let provider = create_provider(json!({ "env": { "ANTHROPIC_BASE_URL": "https://some-proxy.com", "ANTHROPIC_AUTH_TOKEN": "sk-test" }, "auth_mode": "bearer_only" })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Claude, &provider); assert_eq!(provider_type, ProviderType::ClaudeAuth); } #[test] fn test_from_app_type_codex() { let provider = create_provider(json!({ "env": { "OPENAI_API_KEY": "sk-test" } })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Codex, &provider); assert_eq!(provider_type, ProviderType::Codex); } #[test] fn test_from_app_type_gemini_api_key() { let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "AIza-test-key" } })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Gemini, &provider); assert_eq!(provider_type, ProviderType::Gemini); } #[test] fn test_from_app_type_gemini_cli_oauth() { let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "ya29.test-access-token" } })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Gemini, &provider); assert_eq!(provider_type, ProviderType::GeminiCli); } #[test] fn test_from_app_type_gemini_cli_json() { let provider = create_provider(json!({ "env": { "GEMINI_API_KEY": "{\"access_token\":\"ya29.test\",\"refresh_token\":\"1//test\"}" } })); let provider_type = ProviderType::from_app_type_and_config(&AppType::Gemini, &provider); assert_eq!(provider_type, ProviderType::GeminiCli); } #[test] fn test_get_adapter_for_provider_type() { let adapter = get_adapter_for_provider_type(&ProviderType::Claude); assert_eq!(adapter.name(), "Claude"); let adapter = get_adapter_for_provider_type(&ProviderType::ClaudeAuth); assert_eq!(adapter.name(), "Claude"); let adapter = get_adapter_for_provider_type(&ProviderType::OpenRouter); assert_eq!(adapter.name(), "Claude"); let adapter = get_adapter_for_provider_type(&ProviderType::GitHubCopilot); assert_eq!(adapter.name(), "Claude"); let adapter = get_adapter_for_provider_type(&ProviderType::Codex); assert_eq!(adapter.name(), "Codex"); let adapter = get_adapter_for_provider_type(&ProviderType::Gemini); assert_eq!(adapter.name(), "Gemini"); let adapter = get_adapter_for_provider_type(&ProviderType::GeminiCli); assert_eq!(adapter.name(), "Gemini"); } }