Files
cc-switch/src-tauri/src/provider.rs
YoVinchen fe4a968eef feat(provider): add FormatTransformConfig for API format conversion
Add FormatTransformConfig struct to support API format transformation
between different providers (e.g., Anthropic ↔ OpenAI). This enables
providers like OpenRouter that use OpenAI-compatible interfaces.
2026-01-25 19:46:22 +08:00

637 lines
22 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "createdAt")]
pub created_at: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "sortIndex")]
pub sort_index: Option<usize>,
/// 备注信息
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
/// 供应商元数据(不写入 live 配置,仅存于 ~/.cc-switch/config.json
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ProviderMeta>,
/// 图标名称(如 "openai", "anthropic"
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
/// 图标颜色(Hex 格式,如 "#00A67E"
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "iconColor")]
pub icon_color: Option<String>,
/// 是否加入故障转移队列
#[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<String>,
) -> 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<String, Provider>,
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<u64>,
/// 用量查询专用的 API Key(通用模板使用)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "apiKey")]
pub api_key: Option<String>,
/// 用量查询专用的 Base URL(通用和 NewAPI 模板使用)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "baseUrl")]
pub base_url: Option<String>,
/// 访问令牌(用于需要登录的接口,NewAPI 模板使用)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "accessToken")]
pub access_token: Option<String>,
/// 用户ID(用于需要用户标识的接口,NewAPI 模板使用)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "userId")]
pub user_id: Option<String>,
/// 模板类型(用于后端判断验证规则)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "templateType")]
pub template_type: Option<String>,
/// 自动查询间隔(单位:分钟,0 表示禁用自动查询)
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "autoQueryInterval")]
pub auto_query_interval: Option<u64>,
}
/// 用量数据
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageData {
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "planName")]
pub plan_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extra: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "isValid")]
pub is_valid: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "invalidMessage")]
pub invalid_message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub used: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remaining: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unit: Option<String>,
}
/// 用量查询结果(支持多套餐)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UsageResult {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Vec<UsageData>>, // 支持返回多个套餐
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
/// 供应商单独的模型测试配置
#[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<String>,
/// 超时时间(秒)
#[serde(rename = "timeoutSecs", skip_serializing_if = "Option::is_none")]
pub timeout_secs: Option<u64>,
/// 测试提示词
#[serde(rename = "testPrompt", skip_serializing_if = "Option::is_none")]
pub test_prompt: Option<String>,
/// 降级阈值(毫秒)
#[serde(
rename = "degradedThresholdMs",
skip_serializing_if = "Option::is_none"
)]
pub degraded_threshold_ms: Option<u64>,
/// 最大重试次数
#[serde(rename = "maxRetries", skip_serializing_if = "Option::is_none")]
pub max_retries: Option<u32>,
}
/// 供应商单独的代理配置
#[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<String>,
/// 代理主机
#[serde(rename = "proxyHost", skip_serializing_if = "Option::is_none")]
pub proxy_host: Option<String>,
/// 代理端口
#[serde(rename = "proxyPort", skip_serializing_if = "Option::is_none")]
pub proxy_port: Option<u16>,
/// 代理用户名(可选)
#[serde(rename = "proxyUsername", skip_serializing_if = "Option::is_none")]
pub proxy_username: Option<String>,
/// 代理密码(可选)
#[serde(rename = "proxyPassword", skip_serializing_if = "Option::is_none")]
pub proxy_password: Option<String>,
}
/// 格式转换配置(用于 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<String>,
/// 目标格式:anthropic, openai, gemini
#[serde(rename = "targetFormat", skip_serializing_if = "Option::is_none")]
pub target_format: Option<String>,
/// 是否转换流式响应(默认 true)
#[serde(rename = "transformStreaming", skip_serializing_if = "Option::is_none")]
pub transform_streaming: Option<bool>,
}
/// 供应商元数据
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ProviderMeta {
/// 自定义端点列表(按 URL 去重存储)
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub custom_endpoints: HashMap<String, crate::settings::CustomEndpoint>,
/// 用量查询脚本配置
#[serde(skip_serializing_if = "Option::is_none")]
pub usage_script: Option<UsageScript>,
/// 请求地址管理:测速后自动选择最佳端点
#[serde(rename = "endpointAutoSelect", skip_serializing_if = "Option::is_none")]
pub endpoint_auto_select: Option<bool>,
/// 合作伙伴标记(前端使用 isPartner,保持字段名一致)
#[serde(rename = "isPartner", skip_serializing_if = "Option::is_none")]
pub is_partner: Option<bool>,
/// 合作伙伴促销 key,用于识别 PackyCode 等特殊供应商
#[serde(
rename = "partnerPromotionKey",
skip_serializing_if = "Option::is_none"
)]
pub partner_promotion_key: Option<String>,
/// 成本倍数(用于计算实际成本)
#[serde(rename = "costMultiplier", skip_serializing_if = "Option::is_none")]
pub cost_multiplier: Option<String>,
/// 每日消费限额(USD
#[serde(rename = "limitDailyUsd", skip_serializing_if = "Option::is_none")]
pub limit_daily_usd: Option<String>,
/// 每月消费限额(USD
#[serde(rename = "limitMonthlyUsd", skip_serializing_if = "Option::is_none")]
pub limit_monthly_usd: Option<String>,
/// 供应商单独的模型测试配置
#[serde(rename = "testConfig", skip_serializing_if = "Option::is_none")]
pub test_config: Option<ProviderTestConfig>,
/// 供应商单独的代理配置
#[serde(rename = "proxyConfig", skip_serializing_if = "Option::is_none")]
pub proxy_config: Option<ProviderProxyConfig>,
/// 格式转换配置(用于 OpenRouter 等需要 API 格式转换的供应商)
#[serde(rename = "formatTransform", skip_serializing_if = "Option::is_none")]
pub format_transform: Option<FormatTransformConfig>,
}
impl ProviderManager {
/// 获取所有供应商
pub fn get_all_providers(&self) -> &IndexMap<String, Provider> {
&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<String>,
/// Haiku 默认模型
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "haikuModel")]
pub haiku_model: Option<String>,
/// Sonnet 默认模型
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "sonnetModel")]
pub sonnet_model: Option<String>,
/// Opus 默认模型
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "opusModel")]
pub opus_model: Option<String>,
}
/// Codex 模型配置
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CodexModelConfig {
/// 模型名称
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// 推理强度
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "reasoningEffort")]
pub reasoning_effort: Option<String>,
}
/// Gemini 模型配置
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct GeminiModelConfig {
/// 模型名称
#[serde(skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
}
/// 各应用的模型配置
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct UniversalProviderModels {
#[serde(skip_serializing_if = "Option::is_none")]
pub claude: Option<ClaudeModelConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub codex: Option<CodexModelConfig>,
#[serde(skip_serializing_if = "Option::is_none")]
pub gemini: Option<GeminiModelConfig>,
}
/// 统一供应商(跨应用共享配置)
#[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<String>,
/// 备注信息
#[serde(skip_serializing_if = "Option::is_none")]
pub notes: Option<String>,
/// 图标名称
#[serde(skip_serializing_if = "Option::is_none")]
pub icon: Option<String>,
/// 图标颜色
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "iconColor")]
pub icon_color: Option<String>,
/// 元数据
#[serde(skip_serializing_if = "Option::is_none")]
pub meta: Option<ProviderMeta>,
/// 创建时间戳
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "createdAt")]
pub created_at: Option<i64>,
/// 排序索引
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "sortIndex")]
pub sort_index: Option<usize>,
}
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<Provider> {
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<Provider> {
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<Provider> {
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<String>,
/// 供应商选项(API 密钥、基础 URL 等)
#[serde(default)]
pub options: OpenCodeProviderOptions,
/// 模型定义映射
#[serde(default)]
pub models: HashMap<String, OpenCodeModel>,
}
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<String>,
/// API 密钥(支持环境变量引用,如 "{env:API_KEY}"
#[serde(rename = "apiKey", skip_serializing_if = "Option::is_none")]
pub api_key: Option<String>,
/// 自定义请求头
#[serde(skip_serializing_if = "Option::is_none")]
pub headers: Option<HashMap<String, String>>,
/// 额外选项(timeout, setCacheKey 等)
/// 使用 flatten 捕获所有未明确定义的字段
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, Value>,
}
/// OpenCode 模型定义
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OpenCodeModel {
/// 模型显示名称
pub name: String,
/// 模型限制(上下文和输出 token 数)
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<OpenCodeModelLimit>,
/// 模型额外选项(provider 路由等)
#[serde(skip_serializing_if = "Option::is_none")]
pub options: Option<HashMap<String, Value>>,
/// 额外字段(cost、modalities、thinking、variants 等)
/// 使用 flatten 捕获所有未明确定义的字段
#[serde(flatten, default, skip_serializing_if = "HashMap::is_empty")]
pub extra: HashMap<String, Value>,
}
/// OpenCode 模型限制
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct OpenCodeModelLimit {
/// 上下文 token 限制
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<u64>,
/// 输出 token 限制
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<u64>,
}