use indexmap::IndexMap; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::HashMap; // SSOT 模式:不再写供应商副本文件 /// 供应商结构体 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Provider { pub id: String, pub name: String, #[serde(rename = "settingsConfig")] pub settings_config: Value, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "websiteUrl")] pub website_url: Option, #[serde(skip_serializing_if = "Option::is_none")] pub category: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "createdAt")] pub created_at: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "sortIndex")] pub sort_index: Option, /// 备注信息 #[serde(skip_serializing_if = "Option::is_none")] pub notes: Option, /// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json) #[serde(skip_serializing_if = "Option::is_none")] pub meta: Option, /// 图标名称(如 "openai", "anthropic") #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, /// 图标颜色(Hex 格式,如 "#00A67E") #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "iconColor")] pub icon_color: Option, /// 是否加入故障转移队列 #[serde(default)] #[serde(rename = "inFailoverQueue")] pub in_failover_queue: bool, } impl Provider { /// 从现有ID创建供应商 pub fn with_id( id: String, name: String, settings_config: Value, website_url: Option, ) -> Self { Self { id, name, settings_config, website_url, category: None, created_at: None, sort_index: None, notes: None, meta: None, icon: None, icon_color: None, in_failover_queue: false, } } } /// 供应商管理器 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderManager { pub providers: IndexMap, pub current: String, } /// 用量查询脚本配置 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsageScript { pub enabled: bool, pub language: String, pub code: String, #[serde(skip_serializing_if = "Option::is_none")] pub timeout: Option, /// 用量查询专用的 API Key(通用模板使用) #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "apiKey")] pub api_key: Option, /// 用量查询专用的 Base URL(通用和 NewAPI 模板使用) #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "baseUrl")] pub base_url: Option, /// 访问令牌(用于需要登录的接口,NewAPI 模板使用) #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "accessToken")] pub access_token: Option, /// 用户ID(用于需要用户标识的接口,NewAPI 模板使用) #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "userId")] pub user_id: Option, /// 模板类型(用于后端判断验证规则) #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "templateType")] pub template_type: Option, /// 自动查询间隔(单位:分钟,0 表示禁用自动查询) #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "autoQueryInterval")] pub auto_query_interval: Option, } /// 用量数据 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsageData { #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "planName")] pub plan_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub extra: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "isValid")] pub is_valid: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "invalidMessage")] pub invalid_message: Option, #[serde(skip_serializing_if = "Option::is_none")] pub total: Option, #[serde(skip_serializing_if = "Option::is_none")] pub used: Option, #[serde(skip_serializing_if = "Option::is_none")] pub remaining: Option, #[serde(skip_serializing_if = "Option::is_none")] pub unit: Option, } /// 用量查询结果(支持多套餐) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UsageResult { pub success: bool, #[serde(skip_serializing_if = "Option::is_none")] pub data: Option>, // 支持返回多个套餐 #[serde(skip_serializing_if = "Option::is_none")] pub error: Option, } /// 供应商单独的模型测试配置 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderTestConfig { /// 是否启用单独配置(false 时使用全局配置) #[serde(default)] pub enabled: bool, /// 测试用的模型名称(覆盖全局配置) #[serde(rename = "testModel", skip_serializing_if = "Option::is_none")] pub test_model: Option, /// 超时时间(秒) #[serde(rename = "timeoutSecs", skip_serializing_if = "Option::is_none")] pub timeout_secs: Option, /// 测试提示词 #[serde(rename = "testPrompt", skip_serializing_if = "Option::is_none")] pub test_prompt: Option, /// 降级阈值(毫秒) #[serde( rename = "degradedThresholdMs", skip_serializing_if = "Option::is_none" )] pub degraded_threshold_ms: Option, /// 最大重试次数 #[serde(rename = "maxRetries", skip_serializing_if = "Option::is_none")] pub max_retries: Option, } /// 供应商单独的代理配置 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderProxyConfig { /// 是否启用单独配置(false 时使用全局/系统代理) #[serde(default)] pub enabled: bool, /// 代理类型:http, https, socks5 #[serde(rename = "proxyType", skip_serializing_if = "Option::is_none")] pub proxy_type: Option, /// 代理主机 #[serde(rename = "proxyHost", skip_serializing_if = "Option::is_none")] pub proxy_host: Option, /// 代理端口 #[serde(rename = "proxyPort", skip_serializing_if = "Option::is_none")] pub proxy_port: Option, /// 代理用户名(可选) #[serde(rename = "proxyUsername", skip_serializing_if = "Option::is_none")] pub proxy_username: Option, /// 代理密码(可选) #[serde(rename = "proxyPassword", skip_serializing_if = "Option::is_none")] pub proxy_password: Option, } /// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商) #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct FormatTransformConfig { /// 是否启用格式转换 #[serde(default)] pub enabled: bool, /// 源格式:anthropic, openai, gemini #[serde(rename = "sourceFormat", skip_serializing_if = "Option::is_none")] pub source_format: Option, /// 目标格式:anthropic, openai, gemini #[serde(rename = "targetFormat", skip_serializing_if = "Option::is_none")] pub target_format: Option, /// 是否转换流式响应(默认 true) #[serde(rename = "transformStreaming", skip_serializing_if = "Option::is_none")] pub transform_streaming: Option, } /// 供应商元数据 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ProviderMeta { /// 自定义端点列表(按 URL 去重存储) #[serde(default, skip_serializing_if = "HashMap::is_empty")] pub custom_endpoints: HashMap, /// 用量查询脚本配置 #[serde(skip_serializing_if = "Option::is_none")] pub usage_script: Option, /// 请求地址管理:测速后自动选择最佳端点 #[serde(rename = "endpointAutoSelect", skip_serializing_if = "Option::is_none")] pub endpoint_auto_select: Option, /// 合作伙伴标记(前端使用 isPartner,保持字段名一致) #[serde(rename = "isPartner", skip_serializing_if = "Option::is_none")] pub is_partner: Option, /// 合作伙伴促销 key,用于识别 PackyCode 等特殊供应商 #[serde( rename = "partnerPromotionKey", skip_serializing_if = "Option::is_none" )] pub partner_promotion_key: Option, /// 成本倍数(用于计算实际成本) #[serde(rename = "costMultiplier", skip_serializing_if = "Option::is_none")] pub cost_multiplier: Option, /// 每日消费限额(USD) #[serde(rename = "limitDailyUsd", skip_serializing_if = "Option::is_none")] pub limit_daily_usd: Option, /// 每月消费限额(USD) #[serde(rename = "limitMonthlyUsd", skip_serializing_if = "Option::is_none")] pub limit_monthly_usd: Option, /// 供应商单独的模型测试配置 #[serde(rename = "testConfig", skip_serializing_if = "Option::is_none")] pub test_config: Option, /// 供应商单独的代理配置 #[serde(rename = "proxyConfig", skip_serializing_if = "Option::is_none")] pub proxy_config: Option, /// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商) #[serde(rename = "formatTransform", skip_serializing_if = "Option::is_none")] pub format_transform: Option, } impl ProviderManager { /// 获取所有供应商 pub fn get_all_providers(&self) -> &IndexMap { &self.providers } } // ============================================================================ // 统一供应商(Universal Provider)- 跨应用共享配置 // ============================================================================ /// 统一供应商的应用启用状态 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct UniversalProviderApps { #[serde(default)] pub claude: bool, #[serde(default)] pub codex: bool, #[serde(default)] pub gemini: bool, } /// Claude 模型配置 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct ClaudeModelConfig { /// 主模型 #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// Haiku 默认模型 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "haikuModel")] pub haiku_model: Option, /// Sonnet 默认模型 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "sonnetModel")] pub sonnet_model: Option, /// Opus 默认模型 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "opusModel")] pub opus_model: Option, } /// Codex 模型配置 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct CodexModelConfig { /// 模型名称 #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, /// 推理强度 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "reasoningEffort")] pub reasoning_effort: Option, } /// Gemini 模型配置 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct GeminiModelConfig { /// 模型名称 #[serde(skip_serializing_if = "Option::is_none")] pub model: Option, } /// 各应用的模型配置 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct UniversalProviderModels { #[serde(skip_serializing_if = "Option::is_none")] pub claude: Option, #[serde(skip_serializing_if = "Option::is_none")] pub codex: Option, #[serde(skip_serializing_if = "Option::is_none")] pub gemini: Option, } /// 统一供应商(跨应用共享配置) #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UniversalProvider { /// 唯一标识 pub id: String, /// 供应商名称 pub name: String, /// 供应商类型(如 "newapi", "custom") #[serde(rename = "providerType")] pub provider_type: String, /// 应用启用状态 pub apps: UniversalProviderApps, /// API 基础地址 #[serde(rename = "baseUrl")] pub base_url: String, /// API 密钥 #[serde(rename = "apiKey")] pub api_key: String, /// 各应用的模型配置 #[serde(default)] pub models: UniversalProviderModels, /// 网站链接 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "websiteUrl")] pub website_url: Option, /// 备注信息 #[serde(skip_serializing_if = "Option::is_none")] pub notes: Option, /// 图标名称 #[serde(skip_serializing_if = "Option::is_none")] pub icon: Option, /// 图标颜色 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "iconColor")] pub icon_color: Option, /// 元数据 #[serde(skip_serializing_if = "Option::is_none")] pub meta: Option, /// 创建时间戳 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "createdAt")] pub created_at: Option, /// 排序索引 #[serde(skip_serializing_if = "Option::is_none")] #[serde(rename = "sortIndex")] pub sort_index: Option, } impl UniversalProvider { /// 创建新的统一供应商 pub fn new( id: String, name: String, provider_type: String, base_url: String, api_key: String, ) -> Self { Self { id, name, provider_type, apps: UniversalProviderApps::default(), base_url, api_key, models: UniversalProviderModels::default(), website_url: None, notes: None, icon: None, icon_color: None, meta: None, created_at: Some(chrono::Utc::now().timestamp_millis()), sort_index: None, } } /// 生成 Claude 供应商配置 pub fn to_claude_provider(&self) -> Option { if !self.apps.claude { return None; } let models = self.models.claude.as_ref(); let model = models .and_then(|m| m.model.clone()) .unwrap_or_else(|| "claude-sonnet-4-20250514".to_string()); let haiku = models .and_then(|m| m.haiku_model.clone()) .unwrap_or_else(|| model.clone()); let sonnet = models .and_then(|m| m.sonnet_model.clone()) .unwrap_or_else(|| model.clone()); let opus = models .and_then(|m| m.opus_model.clone()) .unwrap_or_else(|| model.clone()); let settings_config = serde_json::json!({ "env": { "ANTHROPIC_BASE_URL": self.base_url, "ANTHROPIC_AUTH_TOKEN": self.api_key, "ANTHROPIC_MODEL": model, "ANTHROPIC_DEFAULT_HAIKU_MODEL": haiku, "ANTHROPIC_DEFAULT_SONNET_MODEL": sonnet, "ANTHROPIC_DEFAULT_OPUS_MODEL": opus, } }); Some(Provider { id: format!("universal-claude-{}", self.id), name: self.name.clone(), settings_config, website_url: self.website_url.clone(), category: Some("aggregator".to_string()), created_at: self.created_at, sort_index: self.sort_index, notes: self.notes.clone(), meta: self.meta.clone(), icon: self.icon.clone(), icon_color: self.icon_color.clone(), in_failover_queue: false, }) } /// 生成 Codex 供应商配置 pub fn to_codex_provider(&self) -> Option { if !self.apps.codex { return None; } let models = self.models.codex.as_ref(); let model = models .and_then(|m| m.model.clone()) .unwrap_or_else(|| "gpt-4o".to_string()); let reasoning_effort = models .and_then(|m| m.reasoning_effort.clone()) .unwrap_or_else(|| "high".to_string()); // 确保 base_url 以 /v1 结尾(Codex 使用 OpenAI 兼容 API) let codex_base_url = if self.base_url.ends_with("/v1") { self.base_url.clone() } else { format!("{}/v1", self.base_url.trim_end_matches('/')) }; // 生成 Codex 的 config.toml 内容 let config_toml = format!( r#"model_provider = "newapi" model = "{model}" model_reasoning_effort = "{reasoning_effort}" disable_response_storage = true [model_providers.newapi] name = "NewAPI" base_url = "{codex_base_url}" wire_api = "responses" requires_openai_auth = true"# ); let settings_config = serde_json::json!({ "auth": { "OPENAI_API_KEY": self.api_key }, "config": config_toml }); Some(Provider { id: format!("universal-codex-{}", self.id), name: self.name.clone(), settings_config, website_url: self.website_url.clone(), category: Some("aggregator".to_string()), created_at: self.created_at, sort_index: self.sort_index, notes: self.notes.clone(), meta: self.meta.clone(), icon: self.icon.clone(), icon_color: self.icon_color.clone(), in_failover_queue: false, }) } /// 生成 Gemini 供应商配置 pub fn to_gemini_provider(&self) -> Option { if !self.apps.gemini { return None; } let models = self.models.gemini.as_ref(); let model = models .and_then(|m| m.model.clone()) .unwrap_or_else(|| "gemini-2.5-pro".to_string()); let settings_config = serde_json::json!({ "env": { "GOOGLE_GEMINI_BASE_URL": self.base_url, "GEMINI_API_KEY": self.api_key, "GEMINI_MODEL": model, } }); Some(Provider { id: format!("universal-gemini-{}", self.id), name: self.name.clone(), settings_config, website_url: self.website_url.clone(), category: Some("aggregator".to_string()), created_at: self.created_at, sort_index: self.sort_index, notes: self.notes.clone(), meta: self.meta.clone(), icon: self.icon.clone(), icon_color: self.icon_color.clone(), in_failover_queue: false, }) } } // ============================================================================ // OpenCode 供应商配置结构 // ============================================================================ /// OpenCode 供应商的 settings_config 结构 /// /// OpenCode 使用 AI SDK 包名来指定供应商类型,与其他应用的配置格式不同。 /// 配置示例: /// ```json /// { /// "npm": "@ai-sdk/openai-compatible", /// "options": { "baseURL": "https://api.example.com/v1", "apiKey": "sk-xxx" }, /// "models": { "gpt-4o": { "name": "GPT-4o" } } /// } /// ``` #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenCodeProviderConfig { /// AI SDK 包名,如 "@ai-sdk/openai-compatible", "@ai-sdk/anthropic" pub npm: String, /// 供应商名称(可选,用于显示) #[serde(skip_serializing_if = "Option::is_none")] pub name: Option, /// 供应商选项(API 密钥、基础 URL 等) #[serde(default)] pub options: OpenCodeProviderOptions, /// 模型定义映射 #[serde(default)] pub models: HashMap, } impl Default for OpenCodeProviderConfig { fn default() -> Self { Self { npm: "@ai-sdk/openai-compatible".to_string(), name: None, options: OpenCodeProviderOptions::default(), models: HashMap::new(), } } } /// OpenCode 供应商选项 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OpenCodeProviderOptions { /// API 基础 URL #[serde(rename = "baseURL", skip_serializing_if = "Option::is_none")] pub base_url: Option, /// API 密钥(支持环境变量引用,如 "{env:API_KEY}") #[serde(rename = "apiKey", skip_serializing_if = "Option::is_none")] pub api_key: Option, /// 自定义请求头 #[serde(skip_serializing_if = "Option::is_none")] pub headers: Option>, /// 额外选项(timeout, setCacheKey 等) /// 使用 flatten 捕获所有未明确定义的字段 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")] pub extra: HashMap, } /// OpenCode 模型定义 #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OpenCodeModel { /// 模型显示名称 pub name: String, /// 模型限制(上下文和输出 token 数) #[serde(skip_serializing_if = "Option::is_none")] pub limit: Option, /// 模型额外选项(provider 路由等) #[serde(skip_serializing_if = "Option::is_none")] pub options: Option>, /// 额外字段(cost、modalities、thinking、variants 等) /// 使用 flatten 捕获所有未明确定义的字段 #[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")] pub extra: HashMap, } /// OpenCode 模型限制 #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct OpenCodeModelLimit { /// 上下文 token 限制 #[serde(skip_serializing_if = "Option::is_none")] pub context: Option, /// 输出 token 限制 #[serde(skip_serializing_if = "Option::is_none")] pub output: Option, }