app / src-tauri /src /proxy /common /model_mapping.rs
AZILS's picture
Upload 323 files
a21c316 verified
// 模型名称映射
use std::collections::HashMap;
use once_cell::sync::Lazy;
use dashmap::DashMap;
// 动态官方废弃模型转发表 (old_model_id -> new_model_id)
pub static DYNAMIC_MODEL_FORWARDING_RULES: Lazy<DashMap<String, String>> = Lazy::new(|| DashMap::new());
pub fn update_dynamic_forwarding_rules(old_model: String, new_model: String) {
if !DYNAMIC_MODEL_FORWARDING_RULES.contains_key(&old_model) {
crate::modules::logger::log_info(&format!("[Mapping] Registered automatic forwarding rule: {} -> {}", old_model, new_model));
}
DYNAMIC_MODEL_FORWARDING_RULES.insert(old_model, new_model);
}
static CLAUDE_TO_GEMINI: Lazy<HashMap<&'static str, &'static str>> = Lazy::new(|| {
let mut m = HashMap::new();
// 直接支持的模型
m.insert("claude-sonnet-4-6", "claude-sonnet-4-6");
m.insert("claude-sonnet-4-6-thinking", "claude-sonnet-4-6-thinking");
// [Redirect] Sonnet 4.5 -> Sonnet 4.6
m.insert("claude-sonnet-4-5", "claude-sonnet-4-6");
m.insert("claude-sonnet-4-5-thinking", "claude-sonnet-4-6-thinking");
// 别名映射
m.insert("claude-sonnet-4-5-20250929", "claude-sonnet-4-6-thinking");
m.insert("claude-3-5-sonnet-20241022", "claude-sonnet-4-6");
m.insert("claude-3-5-sonnet-20240620", "claude-sonnet-4-6");
// [Redirect] Opus 4.5 -> Opus 4.6 (Issue #1743)
m.insert("claude-opus-4", "claude-opus-4-6-thinking");
m.insert("claude-opus-4-5-thinking", "claude-opus-4-6-thinking");
m.insert("claude-opus-4-5-20251101", "claude-opus-4-6-thinking");
// Claude Opus 4.6
m.insert("claude-opus-4-6-thinking", "claude-opus-4-6-thinking");
m.insert("claude-opus-4-6", "claude-opus-4-6-thinking");
m.insert("claude-opus-4-6-20260201", "claude-opus-4-6-thinking");
m.insert("claude-haiku-4", "claude-sonnet-4-6");
m.insert("claude-3-haiku-20240307", "claude-sonnet-4-6");
m.insert("claude-haiku-4-5-20251001", "claude-sonnet-4-6");
// OpenAI 协议映射表
m.insert("gpt-4", "gemini-2.5-flash");
m.insert("gpt-4-turbo", "gemini-2.5-flash");
m.insert("gpt-4-turbo-preview", "gemini-2.5-flash");
m.insert("gpt-4-0125-preview", "gemini-2.5-flash");
m.insert("gpt-4-1106-preview", "gemini-2.5-flash");
m.insert("gpt-4-0613", "gemini-2.5-flash");
m.insert("gpt-4o", "gemini-2.5-flash");
m.insert("gpt-4o-2024-05-13", "gemini-2.5-flash");
m.insert("gpt-4o-2024-08-06", "gemini-2.5-flash");
m.insert("gpt-4o-mini", "gemini-2.5-flash");
m.insert("gpt-4o-mini-2024-07-18", "gemini-2.5-flash");
m.insert("gpt-3.5-turbo", "gemini-2.5-flash");
m.insert("gpt-3.5-turbo-16k", "gemini-2.5-flash");
m.insert("gpt-3.5-turbo-0125", "gemini-2.5-flash");
m.insert("gpt-3.5-turbo-1106", "gemini-2.5-flash");
m.insert("gpt-3.5-turbo-0613", "gemini-2.5-flash");
// Gemini 协议映射表
m.insert("gemini-2.5-flash-lite", "gemini-2.5-flash");
m.insert("gemini-2.5-flash-thinking", "gemini-2.5-flash-thinking");
// Gemini Pro family:
// - Concrete model IDs should pass through unchanged.
// - Generic aliases (without tier) still route to preview as fallback entrypoint.
m.insert("gemini-3.1-pro-low", "gemini-3.1-pro-low");
m.insert("gemini-3.1-pro-high", "gemini-3.1-pro-high");
m.insert("gemini-3.1-pro-preview", "gemini-3.1-pro-preview");
m.insert("gemini-3.1-pro", "gemini-3.1-pro-preview");
m.insert("gemini-3-pro-low", "gemini-3-pro-low");
m.insert("gemini-3-pro-high", "gemini-3-pro-high");
m.insert("gemini-3-pro-preview", "gemini-3-pro-preview");
m.insert("gemini-3-pro", "gemini-3-pro-preview");
m.insert("gemini-2.5-flash", "gemini-2.5-flash");
m.insert("gemini-3-flash", "gemini-3-flash");
m.insert("gemini-3-pro-image", "gemini-3-pro-image");
// [New] Unified Virtual ID for Background Tasks (Title, Summary, etc.)
// Allows users to override all background tasks via custom_mapping
m.insert("internal-background-task", "gemini-2.5-flash");
m
});
/// Map Claude model names to Gemini model names
///
/// # 映射策略
/// 1. **精确匹配**: 检查 CLAUDE_TO_GEMINI 映射表
/// 2. **已知前缀透传**: gemini-* 和 *-thinking 模型直接透传
/// 3. **[NEW] 直接透传**: 未知模型 ID 直接传递给 Google API (支持体验未发布模型)
///
/// # 参数
/// - `input`: 原始模型名称
///
/// # 返回
/// 映射后的目标模型名称
///
/// # 示例
/// ```
/// // 精确匹配
/// assert_eq!(map_claude_model_to_gemini("claude-opus-4"), "claude-opus-4-5-thinking");
///
/// // Gemini 模型透传
/// assert_eq!(map_claude_model_to_gemini("gemini-2.5-flash"), "gemini-2.5-flash");
///
/// // 直接透传未知模型 (NEW!)
/// assert_eq!(map_claude_model_to_gemini("claude-opus-4-6"), "claude-opus-4-6");
/// assert_eq!(map_claude_model_to_gemini("claude-sonnet-5"), "claude-sonnet-5");
/// ```
pub fn map_claude_model_to_gemini(input: &str) -> String {
// 1. Check exact match in map
if let Some(mapped) = CLAUDE_TO_GEMINI.get(input) {
return mapped.to_string();
}
// 2. Pass-through known prefixes (gemini-, -thinking) to support dynamic suffixes
if input.starts_with("gemini-") || input.contains("thinking") {
return input.to_string();
}
// 3. [ENHANCED] 直接透传未知模型 ID,而不是强制 fallback
// 这允许用户通过自定义映射体验未发布的模型 (如 claude-opus-4-6)
// Google API 会自动处理无效模型并返回错误,用户可以根据错误调整映射
input.to_string()
}
/// 获取所有内置支持的模型列表关键字
pub fn get_supported_models() -> Vec<String> {
CLAUDE_TO_GEMINI.keys().map(|s| s.to_string()).collect()
}
/// 动态获取所有可用模型列表 (包含内置与用户自定义与官方端点动态下发)
pub async fn get_all_dynamic_models(
custom_mapping: &tokio::sync::RwLock<std::collections::HashMap<String, String>>,
token_manager: Option<&crate::proxy::token_manager::TokenManager>,
) -> Vec<String> {
use std::collections::HashSet;
let mut model_ids = HashSet::new();
// 1. 获取所有内置映射模型
for m in get_supported_models() {
model_ids.insert(m);
}
// 2. 获取所有自定义映射模型 (Custom)
{
let mapping = custom_mapping.read().await;
for key in mapping.keys() {
model_ids.insert(key.clone());
}
}
// 3. [NEW] 获取所有账号从官方接口汇聚而来的动态模型
if let Some(tm) = token_manager {
for dynamic_model in tm.get_all_collected_models() {
model_ids.insert(dynamic_model);
}
}
// 5. 确保包含常用的 Gemini/画画模型 ID
model_ids.insert("gemini-3.1-pro-low".to_string());
// [NEW] Issue #247: Dynamically generate all Image Gen Combinations
let base = "gemini-3-pro-image";
let resolutions = vec!["", "-2k", "-4k"];
let ratios = vec!["", "-1x1", "-4x3", "-3x4", "-16x9", "-9x16", "-21x9"];
for res in resolutions {
for ratio in ratios.iter() {
let mut id = base.to_string();
id.push_str(res);
id.push_str(ratio);
model_ids.insert(id);
}
}
model_ids.insert("gemini-2.0-flash-exp".to_string());
model_ids.insert("gemini-2.5-flash".to_string());
// gemini-2.5-pro removed
model_ids.insert("gemini-3-flash".to_string());
model_ids.insert("gemini-3.1-pro-high".to_string());
model_ids.insert("gemini-3.1-pro-low".to_string());
let mut sorted_ids: Vec<_> = model_ids.into_iter().collect();
sorted_ids.sort();
sorted_ids
}
/// Wildcard matching - supports multiple wildcards
///
/// **Note**: Matching is **case-sensitive**. Pattern `GPT-4*` will NOT match `gpt-4-turbo`.
///
/// Examples:
/// - `gpt-4*` matches `gpt-4`, `gpt-4-turbo` ✓
/// - `claude-*-sonnet-*` matches `claude-3-5-sonnet-20241022` ✓
/// - `*-thinking` matches `claude-opus-4-5-thinking` ✓
/// - `a*b*c` matches `a123b456c` ✓
fn wildcard_match(pattern: &str, text: &str) -> bool {
let parts: Vec<&str> = pattern.split('*').collect();
// No wildcard - exact match
if parts.len() == 1 {
return pattern == text;
}
let mut text_pos = 0;
for (i, part) in parts.iter().enumerate() {
if part.is_empty() {
continue; // Skip empty segments from consecutive wildcards
}
if i == 0 {
// First segment must match start
if !text[text_pos..].starts_with(part) {
return false;
}
text_pos += part.len();
} else if i == parts.len() - 1 {
// Last segment must match end
return text[text_pos..].ends_with(part);
} else {
// Middle segments - find next occurrence
if let Some(pos) = text[text_pos..].find(part) {
text_pos += pos + part.len();
} else {
return false;
}
}
}
true
}
/// 核心模型路由解析引擎
/// 优先级:精确匹配 > 通配符匹配 > 系统默认映射
///
/// # 参数
/// - `original_model`: 原始模型名称
/// - `custom_mapping`: 用户自定义映射表
///
/// # 返回
/// 映射后的目标模型名称
pub fn resolve_model_route(
original_model: &str,
custom_mapping: &std::collections::HashMap<String, String>,
) -> String {
// 0. API 热更新废弃模型转发 (最高物理优先级,强制纠正)
// 如果用户非要用已经被移除的模型,并且官方下发了 fallback path,我们在此拦截并纠正
if let Some(forwarded) = DYNAMIC_MODEL_FORWARDING_RULES.get(original_model) {
crate::modules::logger::log_info(&format!("[Router] 官方淘汰重定向: {} -> {}", original_model, forwarded.value()));
return forwarded.value().clone();
}
// 1. 精确匹配 (次高优先级)
if let Some(target) = custom_mapping.get(original_model) {
crate::modules::logger::log_info(&format!("[Router] 精确映射: {} -> {}", original_model, target));
return target.clone();
}
// 2. Wildcard match - most specific (highest non-wildcard chars) wins
// Note: When multiple patterns have the SAME specificity, HashMap iteration order
// determines the result (non-deterministic). Users can avoid this by making patterns
// more specific. Future improvement: use IndexMap + frontend sorting for full control.
let mut best_match: Option<(&str, &str, usize)> = None;
for (pattern, target) in custom_mapping.iter() {
if pattern.contains('*') && wildcard_match(pattern, original_model) {
let specificity = pattern.chars().count() - pattern.matches('*').count();
if best_match.is_none() || specificity > best_match.unwrap().2 {
best_match = Some((pattern.as_str(), target.as_str(), specificity));
}
}
}
if let Some((pattern, target, _)) = best_match {
crate::modules::logger::log_info(&format!(
"[Router] Wildcard match: {} -> {} (rule: {})",
original_model, target, pattern
));
return target.to_string();
}
// 3. 系统默认映射
let result = map_claude_model_to_gemini(original_model);
if result != original_model {
crate::modules::logger::log_info(&format!("[Router] 系统默认映射: {} -> {}", original_model, result));
}
result
}
/// Normalize any physical model name to one of the 3 standard protection IDs.
/// This ensures quota protection works consistently regardless of API versioning or request variations.
///
/// Standard IDs:
/// - `gemini-3-flash`: All Flash variants (1.5-flash, 2.5-flash, 3-flash, etc.)
/// - `gemini-3-pro-high`: All Pro variants (1.5-pro, 2.5-pro, etc.)
/// - `claude-sonnet-4-5`: All Claude Sonnet variants (3-5-sonnet, sonnet-4-5, etc.)
///
/// Returns `None` if the model doesn't match any of the 3 protected categories.
pub fn normalize_to_standard_id(model_name: &str) -> Option<String> {
let lower = model_name.to_lowercase();
// 1. image 资源 (优先匹配,使用 contains 匹配以支持任何变体,如 gemini-3.1-flash-image)
if lower.contains("image") {
return Some("gemini-3-pro-image".to_string());
}
// 2. gemini-3-flash (包含所有 flash 变体)
if lower.contains("flash") {
return Some("gemini-3-flash".to_string());
}
// 3. gemini-3-pro-high (包含 pro 变体)
if lower.contains("pro") && !lower.contains("image") {
return Some("gemini-3-pro-high".to_string());
}
// 4. Claude 系列 (合并 Opus, Sonnet, Haiku 为统一保护组 'claude')
if lower.contains("claude") || lower.contains("opus") || lower.contains("sonnet") || lower.contains("haiku") {
return Some("claude".to_string());
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_model_mapping() {
assert_eq!(
map_claude_model_to_gemini("claude-3-5-sonnet-20241022"),
"claude-sonnet-4-6"
);
// [Redirect] Sonnet 4.5 -> Sonnet 4.6
assert_eq!(
map_claude_model_to_gemini("claude-sonnet-4-5"),
"claude-sonnet-4-6"
);
assert_eq!(
map_claude_model_to_gemini("claude-sonnet-4-5-thinking"),
"claude-sonnet-4-6-thinking"
);
assert_eq!(
map_claude_model_to_gemini("claude-opus-4"),
"claude-opus-4-6-thinking"
);
// Test gemini pass-through (should not be caught by "mini" rule)
assert_eq!(
map_claude_model_to_gemini("gemini-2.5-flash-mini-test"),
"gemini-2.5-flash-mini-test"
);
assert_eq!(map_claude_model_to_gemini("unknown-model"), "unknown-model");
// Gemini Pro concrete IDs should pass through unchanged.
assert_eq!(
map_claude_model_to_gemini("gemini-3-pro-high"),
"gemini-3-pro-high"
);
assert_eq!(
map_claude_model_to_gemini("gemini-3-pro-low"),
"gemini-3-pro-low"
);
assert_eq!(
map_claude_model_to_gemini("gemini-3.1-pro-high"),
"gemini-3.1-pro-high"
);
assert_eq!(
map_claude_model_to_gemini("gemini-3.1-pro-low"),
"gemini-3.1-pro-low"
);
// Generic aliases still map to preview entrypoint.
assert_eq!(
map_claude_model_to_gemini("gemini-3-pro"),
"gemini-3-pro-preview"
);
assert_eq!(
map_claude_model_to_gemini("gemini-3.1-pro"),
"gemini-3.1-pro-preview"
);
// Test Normalization (Opus 4.6 now merged into "claude" group)
assert_eq!(normalize_to_standard_id("claude-opus-4-6-thinking"), Some("claude".to_string()));
assert_eq!(
normalize_to_standard_id("claude-sonnet-4-5"),
Some("claude".to_string())
);
// [Regression] gemini-3-pro-image must NOT be grouped with gemini-3-pro-high
assert_eq!(
normalize_to_standard_id("gemini-3-pro-image"),
Some("gemini-3-pro-image".to_string())
);
assert_eq!(
normalize_to_standard_id("gemini-3-pro-high"),
Some("gemini-3-pro-high".to_string())
);
// [FIX #1955] Test normalization with image suffixes
assert_eq!(
normalize_to_standard_id("gemini-3-pro-image-4k"),
Some("gemini-3-pro-image".to_string())
);
assert_eq!(
normalize_to_standard_id("gemini-3-pro-image-16x9"),
Some("gemini-3-pro-image".to_string())
);
assert_eq!(
normalize_to_standard_id("gemini-3-pro-image-4k-16x9"),
Some("gemini-3-pro-image".to_string())
);
assert_eq!(
normalize_to_standard_id("gemini-3.1-flash-image"),
Some("gemini-3-pro-image".to_string())
);
assert_eq!(
normalize_to_standard_id("gemini-3.1-flash-image-4k"),
Some("gemini-3-pro-image".to_string())
);
}
#[test]
fn test_wildcard_priority() {
let mut custom = HashMap::new();
custom.insert("gpt*".to_string(), "fallback".to_string());
custom.insert("gpt-4*".to_string(), "specific".to_string());
custom.insert("claude-opus-*".to_string(), "opus-default".to_string());
custom.insert("claude-opus*thinking".to_string(), "opus-thinking".to_string());
// More specific pattern wins
assert_eq!(resolve_model_route("gpt-4-turbo", &custom), "specific");
assert_eq!(resolve_model_route("gpt-3.5", &custom), "fallback");
// Suffix constraint is more specific than prefix-only
assert_eq!(resolve_model_route("claude-opus-4-5-thinking", &custom), "opus-thinking");
assert_eq!(resolve_model_route("claude-opus-4", &custom), "opus-default");
}
#[test]
fn test_multi_wildcard_support() {
let mut custom = HashMap::new();
custom.insert("claude-*-sonnet-*".to_string(), "sonnet-versioned".to_string());
custom.insert("gpt-*-*".to_string(), "gpt-multi".to_string());
custom.insert("*thinking*".to_string(), "has-thinking".to_string());
// Multi-wildcard patterns should work
assert_eq!(
resolve_model_route("claude-3-5-sonnet-20241022", &custom),
"sonnet-versioned"
);
assert_eq!(
resolve_model_route("gpt-4-turbo-preview", &custom),
"gpt-multi"
);
assert_eq!(
resolve_model_route("claude-thinking-extended", &custom),
"has-thinking"
);
// Negative case: *thinking* should NOT match models without "thinking"
assert_eq!(
resolve_model_route("random-model-name", &custom),
"random-model-name" // Falls back to system default (pass-through)
);
}
#[test]
fn test_wildcard_edge_cases() {
let mut custom = HashMap::new();
custom.insert("prefix*".to_string(), "prefix-match".to_string());
custom.insert("*".to_string(), "catch-all".to_string());
custom.insert("a*b*c".to_string(), "multi-wild".to_string());
// Specificity: "prefix*" (6) > "*" (0)
assert_eq!(resolve_model_route("prefix-anything", &custom), "prefix-match");
// Catch-all has lowest specificity
assert_eq!(resolve_model_route("random-model", &custom), "catch-all");
// Multi-wildcard: "a*b*c" (3)
assert_eq!(resolve_model_route("a-test-b-foo-c", &custom), "multi-wild");
}
}