| |
| use std::collections::HashMap; |
| use once_cell::sync::Lazy; |
| use dashmap::DashMap; |
|
|
| |
| 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"); |
|
|
| |
| 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"); |
| |
| 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"); |
|
|
| |
| 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"); |
| |
| 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"); |
|
|
| |
| m.insert("gemini-2.5-flash-lite", "gemini-2.5-flash"); |
| m.insert("gemini-2.5-flash-thinking", "gemini-2.5-flash-thinking"); |
| |
| |
| |
| 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"); |
|
|
| |
| |
| m.insert("internal-background-task", "gemini-2.5-flash"); |
|
|
|
|
| m |
| }); |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub fn map_claude_model_to_gemini(input: &str) -> String { |
| |
| if let Some(mapped) = CLAUDE_TO_GEMINI.get(input) { |
| return mapped.to_string(); |
| } |
|
|
| |
| if input.starts_with("gemini-") || input.contains("thinking") { |
| return input.to_string(); |
| } |
|
|
|
|
| |
| |
| |
| 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(); |
|
|
| |
| for m in get_supported_models() { |
| model_ids.insert(m); |
| } |
|
|
| |
| { |
| let mapping = custom_mapping.read().await; |
| for key in mapping.keys() { |
| model_ids.insert(key.clone()); |
| } |
| } |
|
|
| |
| if let Some(tm) = token_manager { |
| for dynamic_model in tm.get_all_collected_models() { |
| model_ids.insert(dynamic_model); |
| } |
| } |
|
|
| |
| model_ids.insert("gemini-3.1-pro-low".to_string()); |
| |
| |
| 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()); |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| fn wildcard_match(pattern: &str, text: &str) -> bool { |
| let parts: Vec<&str> = pattern.split('*').collect(); |
|
|
| |
| if parts.len() == 1 { |
| return pattern == text; |
| } |
|
|
| let mut text_pos = 0; |
|
|
| for (i, part) in parts.iter().enumerate() { |
| if part.is_empty() { |
| continue; |
| } |
|
|
| if i == 0 { |
| |
| if !text[text_pos..].starts_with(part) { |
| return false; |
| } |
| text_pos += part.len(); |
| } else if i == parts.len() - 1 { |
| |
| return text[text_pos..].ends_with(part); |
| } else { |
| |
| if let Some(pos) = text[text_pos..].find(part) { |
| text_pos += pos + part.len(); |
| } else { |
| return false; |
| } |
| } |
| } |
|
|
| true |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub fn resolve_model_route( |
| original_model: &str, |
| custom_mapping: &std::collections::HashMap<String, String>, |
| ) -> String { |
| |
| |
| 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(); |
| } |
|
|
| |
| if let Some(target) = custom_mapping.get(original_model) { |
| crate::modules::logger::log_info(&format!("[Router] 精确映射: {} -> {}", original_model, target)); |
| return target.clone(); |
| } |
| |
| |
| |
| |
| |
| 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(); |
| } |
| |
| |
| let result = map_claude_model_to_gemini(original_model); |
| if result != original_model { |
| crate::modules::logger::log_info(&format!("[Router] 系统默认映射: {} -> {}", original_model, result)); |
| } |
| result |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub fn normalize_to_standard_id(model_name: &str) -> Option<String> { |
| let lower = model_name.to_lowercase(); |
| |
| |
| if lower.contains("image") { |
| return Some("gemini-3-pro-image".to_string()); |
| } |
|
|
| |
| if lower.contains("flash") { |
| return Some("gemini-3-flash".to_string()); |
| } |
|
|
| |
| if lower.contains("pro") && !lower.contains("image") { |
| return Some("gemini-3-pro-high".to_string()); |
| } |
|
|
| |
| 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" |
| ); |
| |
| 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" |
| ); |
| |
| 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"); |
| |
| 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" |
| ); |
| |
| 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" |
| ); |
|
|
| |
| 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()) |
| ); |
|
|
| |
| 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()) |
| ); |
|
|
| |
| 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()); |
|
|
| |
| assert_eq!(resolve_model_route("gpt-4-turbo", &custom), "specific"); |
| assert_eq!(resolve_model_route("gpt-3.5", &custom), "fallback"); |
| |
| 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()); |
|
|
| |
| 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" |
| ); |
|
|
| |
| assert_eq!( |
| resolve_model_route("random-model-name", &custom), |
| "random-model-name" |
| ); |
| } |
|
|
| #[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()); |
|
|
| |
| assert_eq!(resolve_model_route("prefix-anything", &custom), "prefix-match"); |
| |
| assert_eq!(resolve_model_route("random-model", &custom), "catch-all"); |
| |
| assert_eq!(resolve_model_route("a-test-b-foo-c", &custom), "multi-wild"); |
| } |
| } |
|
|