mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 21:30:17 +08:00
5376ea042b
* i18n: update cache terminology across all languages
- Change 'Cache Read' to 'Cache Hit' in all languages
- Change 'Cache Write' to 'Cache Creation' in all languages
- Update zh: 缓存读取 → 缓存命中, 缓存写入 → 缓存创建
- Update en: Cache Read → Cache Hit, Cache Write → Cache Creation
- Update ja: キャッシュ読取 → キャッシュヒット, キャッシュ書込 → キャッシュ作成
Affected keys: cacheReadTokens, cacheCreationTokens, cacheReadCost,
cacheWriteCost, cacheRead, cacheWrite
* feat(usage): add cache metrics to trend chart
- Add cache creation tokens visualization (orange line)
- Add cache hit tokens visualization (purple line)
- Add gradient definitions for new cache metrics
- Include cache data in hourly aggregation
- Display cache metrics alongside input/output tokens
This provides better visibility into cache usage patterns over time.
* fix(usage): fix timezone handling in datetime picker
- Add timestampToLocalDatetime() to convert Unix timestamp to local datetime
- Add localDatetimeToTimestamp() with validation for incomplete input
- Fix issue where typing hours/minutes would jump to previous day
- Validate datetime format completeness before conversion
- Use local timezone instead of UTC for datetime-local input
This resolves the issue where users couldn't fine-tune time selection
and the input would jump unexpectedly when editing hours or minutes.
* feat(usage): add auto-refresh for usage statistics
- Add 30-second auto-refresh interval for all usage queries
- Disable background refresh to save resources
- Apply to: summary, trends, provider stats, model stats, request logs
- Queries automatically update when tab is active
- Pause refresh when user switches to another tab
This keeps usage data fresh without manual refresh.
* fix(proxy): improve usage logging and cache token parsing
- Log requests even when usage parsing fails (with default values)
- Add detailed debug logging for usage metrics
- Support cache_read_input_tokens field in Codex responses
- Fallback to input_tokens_details.cached_tokens if needed
- Add test case for cached_tokens in input_tokens_details
- Ensure all requests are tracked in database for analytics
This fixes missing request logs when API responses lack usage data
and improves cache token detection across different response formats.
* style(rust): use inline format args in format! macros
- Replace format!("...", var) with format!("...{var}")
- Update universal provider ID formatting
- Update error message formatting
- Update config.toml generation in Codex provider
Fixes clippy::uninlined_format_args warnings.
* feat(proxy): enhance provider router logging
- Add debug logs for failover queue provider count
- Log circuit breaker state for each provider check
- Add logs for missing current provider scenarios
- Log when no current provider is configured
- Use inline format args for better readability
This improves debugging of provider selection and failover behavior.
* feat(database): update model pricing data
- Update Claude models to full version format (e.g. claude-opus-4-5-20251101)
- Add GPT-5.2 series model pricing (10 models)
- Add GPT-5.1 series model pricing (10 models)
- Add GPT-5 series model pricing (12 models)
- Add Gemini 3 series model pricing (2 models)
- Update Gemini 2.5 series model ID format (use dot separator)
- Unify display names by removing thinking level suffixes
* fix(usage): correct Gemini output token calculation
Fix Gemini API output token parsing to use totalTokenCount - promptTokenCount
instead of candidatesTokenCount alone. This ensures thoughtsTokenCount is
included in output statistics.
- Update from_gemini_response to calculate output from total - input
- Update from_gemini_stream_chunks with same logic for consistency
- Fix from_codex_stream_events to use adjusted token calculation
- Add test case for responses with thoughtsTokenCount
- Update existing tests to match new calculation logic
* fix(usage): correct cache token billing and add Codex format auto-detection
- Avoid double-billing cache tokens by subtracting from input before calculation
- Add smart Codex parser that auto-detects OpenAI vs Codex API format
- Extract model name from Codex responses for accurate tracking
* fix(proxy): improve takeover detection with live config check
- Add live config takeover detection for hot-switch decision
- Rebuild takeover when backup is missing or placeholder remains
- Make detect_takeover_in_live_config_for_app public
- Fix is_takeover_active to use actual takeover status
* refactor(usage): simplify model pricing lookup by removing suffix fallback
Replace complex suffix-stripping fallback with direct prefix/suffix cleanup.
Model IDs are now cleaned by removing vendor prefix (before /) and colon
suffix (after :), then matched exactly against pricing table.
* feat(database): add Chinese AI model pricing data
Add pricing for domestic AI models (CNY/1M tokens):
- Doubao-Seed-Code (ByteDance)
- DeepSeek V3/V3.1/V3.2
- Kimi K2/K2-Thinking/K2-Turbo (Moonshot)
- MiniMax M2/M2.1/M2.1-Lightning
- GLM-4.6/4.7 (Zhipu)
- Mimo V2 Flash (Xiaomi)
Also fix test case to use correct model ID and remove invalid currency column.
* refactor(proxy): improve header forwarding with blacklist approach
Change from whitelist to blacklist mode for request header forwarding.
Only skip headers that will be overridden (auth, host, content-length).
This preserves client's original headers and improves compatibility.
* fix(proxy): bypass timeout and retry configs when failover is disabled
When auto_failover_enabled is false, timeout and retry configurations
should not affect normal request flow. This change ensures:
- create_forwarder: passes 0 for all timeout/retry params when failover
is disabled, effectively bypassing these checks
- streaming_timeout_config: returns 0 for both first_byte_timeout and
idle_timeout when failover is disabled
This prevents unnecessary timeout errors and retry attempts when users
have explicitly disabled the failover feature.
* fix(proxy): handle zero value input in failover config fields
* refactor(proxy): remove retry logic and add enabled check for failover
* refactor(proxy): distinguish circuit-open from no-provider errors
* Align usage stats to sliding windows
* feat(proxy): add body and header filtering for upstream requests
* feat(proxy): enable transparent passthrough for headers
- Passthrough anthropic-beta header as-is from client
- Passthrough anthropic-version header from client
- Passthrough client IP headers (x-forwarded-for, x-real-ip) by default
- Filter private params (underscore-prefixed fields) from request body
- No database changes required
* feat(proxy): extract session ID from client requests for logging
- Add SessionIdExtractor to parse session ID from Claude/Codex requests
- Support extraction from metadata.user_id, headers, previous_response_id
- Pass session_id through RequestContext to usage logger
- Enable request correlation by session in proxy_request_logs
458 lines
15 KiB
Rust
458 lines
15 KiB
Rust
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>,
|
||
/// 自动查询间隔(单位:分钟,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 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>,
|
||
/// 合作伙伴标记(前端使用 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>,
|
||
}
|
||
|
||
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,
|
||
})
|
||
}
|
||
}
|