| use serde::{Deserialize, Serialize}; |
| |
| use std::collections::HashMap; |
| use std::sync::{OnceLock, RwLock}; |
|
|
| |
| |
| |
|
|
| |
| pub fn normalize_proxy_url(url: &str) -> String { |
| let url = url.trim(); |
| if url.is_empty() { |
| return String::new(); |
| } |
| if !url.contains("://") { |
| format!("http://{}", url) |
| } else { |
| url.to_string() |
| } |
| } |
|
|
| |
| |
| |
| |
| static GLOBAL_THINKING_BUDGET_CONFIG: OnceLock<RwLock<ThinkingBudgetConfig>> = OnceLock::new(); |
|
|
| |
| pub fn get_thinking_budget_config() -> ThinkingBudgetConfig { |
| GLOBAL_THINKING_BUDGET_CONFIG |
| .get() |
| .and_then(|lock| lock.read().ok()) |
| .map(|cfg| cfg.clone()) |
| .unwrap_or_default() |
| } |
|
|
| |
| pub fn update_thinking_budget_config(config: ThinkingBudgetConfig) { |
| if let Some(lock) = GLOBAL_THINKING_BUDGET_CONFIG.get() { |
| if let Ok(mut cfg) = lock.write() { |
| *cfg = config.clone(); |
| tracing::info!( |
| "[Thinking-Budget] Global config updated: mode={:?}, custom_value={}", |
| config.mode, |
| config.custom_value |
| ); |
| } |
| } else { |
| |
| let _ = GLOBAL_THINKING_BUDGET_CONFIG.set(RwLock::new(config.clone())); |
| tracing::info!( |
| "[Thinking-Budget] Global config initialized: mode={:?}, custom_value={}", |
| config.mode, |
| config.custom_value |
| ); |
| } |
| } |
|
|
| |
| |
| |
| |
| static GLOBAL_SYSTEM_PROMPT_CONFIG: OnceLock<RwLock<GlobalSystemPromptConfig>> = OnceLock::new(); |
|
|
| |
| pub fn get_global_system_prompt() -> GlobalSystemPromptConfig { |
| GLOBAL_SYSTEM_PROMPT_CONFIG |
| .get() |
| .and_then(|lock| lock.read().ok()) |
| .map(|cfg| cfg.clone()) |
| .unwrap_or_default() |
| } |
|
|
| |
| pub fn update_global_system_prompt_config(config: GlobalSystemPromptConfig) { |
| if let Some(lock) = GLOBAL_SYSTEM_PROMPT_CONFIG.get() { |
| if let Ok(mut cfg) = lock.write() { |
| *cfg = config.clone(); |
| tracing::info!( |
| "[Global-System-Prompt] Config updated: enabled={}, content_len={}", |
| config.enabled, |
| config.content.len() |
| ); |
| } |
| } else { |
| |
| let _ = GLOBAL_SYSTEM_PROMPT_CONFIG.set(RwLock::new(config.clone())); |
| tracing::info!( |
| "[Global-System-Prompt] Config initialized: enabled={}, content_len={}", |
| config.enabled, |
| config.content.len() |
| ); |
| } |
| } |
|
|
| |
| |
| |
| static GLOBAL_IMAGE_THINKING_MODE: OnceLock<RwLock<String>> = OnceLock::new(); |
|
|
| pub fn get_image_thinking_mode() -> String { |
| GLOBAL_IMAGE_THINKING_MODE |
| .get() |
| .and_then(|lock| lock.read().ok()) |
| .map(|s| s.clone()) |
| .unwrap_or_else(|| "enabled".to_string()) |
| } |
|
|
| pub fn update_image_thinking_mode(mode: Option<String>) { |
| let val = mode.unwrap_or_else(|| "enabled".to_string()); |
| if let Some(lock) = GLOBAL_IMAGE_THINKING_MODE.get() { |
| if let Ok(mut cfg) = lock.write() { |
| if *cfg != val { |
| *cfg = val.clone(); |
| tracing::info!("[Image-Thinking] Global config updated: {}", val); |
| } |
| } |
| } else { |
| let _ = GLOBAL_IMAGE_THINKING_MODE.set(RwLock::new(val.clone())); |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct GlobalSystemPromptConfig { |
| |
| #[serde(default)] |
| pub enabled: bool, |
| |
| #[serde(default)] |
| pub content: String, |
| } |
|
|
| impl Default for GlobalSystemPromptConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| content: String::new(), |
| } |
| } |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ProxyAuthMode { |
| Off, |
| Strict, |
| AllExceptHealth, |
| Auto, |
| } |
|
|
| impl Default for ProxyAuthMode { |
| fn default() -> Self { |
| Self::Auto |
| } |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ZaiDispatchMode { |
| |
| Off, |
| |
| Exclusive, |
| |
| Pooled, |
| |
| Fallback, |
| } |
|
|
| impl Default for ZaiDispatchMode { |
| fn default() -> Self { |
| Self::Off |
| } |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ZaiModelDefaults { |
| |
| #[serde(default = "default_zai_opus_model")] |
| pub opus: String, |
| |
| #[serde(default = "default_zai_sonnet_model")] |
| pub sonnet: String, |
| |
| #[serde(default = "default_zai_haiku_model")] |
| pub haiku: String, |
| } |
|
|
| impl Default for ZaiModelDefaults { |
| fn default() -> Self { |
| Self { |
| opus: default_zai_opus_model(), |
| sonnet: default_zai_sonnet_model(), |
| haiku: default_zai_haiku_model(), |
| } |
| } |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ZaiMcpConfig { |
| #[serde(default)] |
| pub enabled: bool, |
| #[serde(default)] |
| pub web_search_enabled: bool, |
| #[serde(default)] |
| pub web_reader_enabled: bool, |
| #[serde(default)] |
| pub vision_enabled: bool, |
| } |
|
|
| impl Default for ZaiMcpConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| web_search_enabled: false, |
| web_reader_enabled: false, |
| vision_enabled: false, |
| } |
| } |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ZaiConfig { |
| #[serde(default)] |
| pub enabled: bool, |
| #[serde(default = "default_zai_base_url")] |
| pub base_url: String, |
| #[serde(default)] |
| pub api_key: String, |
| #[serde(default)] |
| pub dispatch_mode: ZaiDispatchMode, |
| |
| |
| #[serde(default)] |
| pub model_mapping: HashMap<String, String>, |
| #[serde(default)] |
| pub models: ZaiModelDefaults, |
| #[serde(default)] |
| pub mcp: ZaiMcpConfig, |
| } |
|
|
| impl Default for ZaiConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| base_url: default_zai_base_url(), |
| api_key: String::new(), |
| dispatch_mode: ZaiDispatchMode::Off, |
| model_mapping: HashMap::new(), |
| models: ZaiModelDefaults::default(), |
| mcp: ZaiMcpConfig::default(), |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ExperimentalConfig { |
| |
| #[serde(default = "default_true")] |
| pub enable_signature_cache: bool, |
|
|
| |
| #[serde(default = "default_true")] |
| pub enable_tool_loop_recovery: bool, |
|
|
| |
| #[serde(default = "default_true")] |
| pub enable_cross_model_checks: bool, |
|
|
| |
| |
| |
| #[serde(default = "default_false")] |
| pub enable_usage_scaling: bool, |
|
|
| |
| #[serde(default = "default_threshold_l1")] |
| pub context_compression_threshold_l1: f32, |
|
|
| |
| #[serde(default = "default_threshold_l2")] |
| pub context_compression_threshold_l2: f32, |
|
|
| |
| #[serde(default = "default_threshold_l3")] |
| pub context_compression_threshold_l3: f32, |
| } |
|
|
| impl Default for ExperimentalConfig { |
| fn default() -> Self { |
| Self { |
| enable_signature_cache: true, |
| enable_tool_loop_recovery: true, |
| enable_cross_model_checks: true, |
| enable_usage_scaling: false, |
| context_compression_threshold_l1: 0.4, |
| context_compression_threshold_l2: 0.55, |
| context_compression_threshold_l3: 0.7, |
| } |
| } |
| } |
|
|
| fn default_threshold_l1() -> f32 { |
| 0.4 |
| } |
| fn default_threshold_l2() -> f32 { |
| 0.55 |
| } |
| fn default_threshold_l3() -> f32 { |
| 0.7 |
| } |
|
|
| |
| |
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ThinkingBudgetMode { |
| |
| Auto, |
| |
| Passthrough, |
| |
| Custom, |
| |
| Adaptive, |
| } |
|
|
| impl Default for ThinkingBudgetMode { |
| fn default() -> Self { |
| Self::Auto |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ThinkingBudgetConfig { |
| |
| #[serde(default)] |
| pub mode: ThinkingBudgetMode, |
| |
| #[serde(default = "default_thinking_budget_custom_value")] |
| pub custom_value: u32, |
| |
| #[serde(default, skip_serializing_if = "Option::is_none")] |
| pub effort: Option<String>, |
| } |
|
|
| impl Default for ThinkingBudgetConfig { |
| fn default() -> Self { |
| Self { |
| mode: ThinkingBudgetMode::Auto, |
| custom_value: default_thinking_budget_custom_value(), |
| effort: None, |
| } |
| } |
| } |
|
|
| fn default_thinking_budget_custom_value() -> u32 { |
| 24576 |
| } |
|
|
| fn default_true() -> bool { |
| true |
| } |
|
|
| fn default_false() -> bool { |
| false |
| } |
|
|
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct DebugLoggingConfig { |
| #[serde(default)] |
| pub enabled: bool, |
| #[serde(default)] |
| pub output_dir: Option<String>, |
| } |
|
|
| impl Default for DebugLoggingConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| output_dir: None, |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct IpBlacklistConfig { |
| |
| #[serde(default)] |
| pub enabled: bool, |
|
|
| |
| #[serde(default = "default_block_message")] |
| pub block_message: String, |
| } |
|
|
| impl Default for IpBlacklistConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| block_message: default_block_message(), |
| } |
| } |
| } |
|
|
| fn default_block_message() -> String { |
| "Access denied".to_string() |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct IpWhitelistConfig { |
| |
| #[serde(default)] |
| pub enabled: bool, |
|
|
| |
| #[serde(default = "default_true")] |
| pub whitelist_priority: bool, |
| } |
|
|
| impl Default for IpWhitelistConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| whitelist_priority: true, |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct SecurityMonitorConfig { |
| |
| #[serde(default)] |
| pub blacklist: IpBlacklistConfig, |
|
|
| |
| #[serde(default)] |
| pub whitelist: IpWhitelistConfig, |
| } |
|
|
| impl Default for SecurityMonitorConfig { |
| fn default() -> Self { |
| Self { |
| blacklist: IpBlacklistConfig::default(), |
| whitelist: IpWhitelistConfig::default(), |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ProxyConfig { |
| |
| pub enabled: bool, |
|
|
| |
| |
| |
| #[serde(default)] |
| pub allow_lan_access: bool, |
|
|
| |
| |
| |
| |
| |
| #[serde(default)] |
| pub auth_mode: ProxyAuthMode, |
|
|
| |
| pub port: u16, |
|
|
| |
| pub api_key: String, |
|
|
| |
| pub admin_password: Option<String>, |
|
|
| |
| pub auto_start: bool, |
|
|
| |
| #[serde(default)] |
| pub custom_mapping: std::collections::HashMap<String, String>, |
|
|
| |
| #[serde(default = "default_request_timeout")] |
| pub request_timeout: u64, |
|
|
| |
| #[serde(default)] |
| pub enable_logging: bool, |
|
|
| |
| #[serde(default)] |
| pub debug_logging: DebugLoggingConfig, |
|
|
| |
| #[serde(default)] |
| pub upstream_proxy: UpstreamProxyConfig, |
|
|
| |
| #[serde(default)] |
| pub zai: ZaiConfig, |
|
|
| |
| #[serde(default)] |
| pub user_agent_override: Option<String>, |
|
|
| |
| #[serde(default)] |
| pub scheduling: crate::proxy::sticky_config::StickySessionConfig, |
|
|
| |
| #[serde(default)] |
| pub experimental: ExperimentalConfig, |
|
|
| |
| #[serde(default)] |
| pub security_monitor: SecurityMonitorConfig, |
|
|
| |
| |
| |
| #[serde(default)] |
| pub preferred_account_id: Option<String>, |
|
|
| |
| #[serde(default)] |
| pub saved_user_agent: Option<String>, |
|
|
| |
| |
| #[serde(default)] |
| pub thinking_budget: ThinkingBudgetConfig, |
|
|
| |
| |
| #[serde(default)] |
| pub global_system_prompt: GlobalSystemPromptConfig, |
|
|
| |
| |
| |
| #[serde(default)] |
| pub image_thinking_mode: Option<String>, |
|
|
| |
| #[serde(default)] |
| pub proxy_pool: ProxyPoolConfig, |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize, Default)] |
| pub struct UpstreamProxyConfig { |
| |
| pub enabled: bool, |
| |
| pub url: String, |
| } |
|
|
| impl Default for ProxyConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| allow_lan_access: false, |
| auth_mode: ProxyAuthMode::default(), |
| port: 8045, |
| api_key: format!("sk-{}", uuid::Uuid::new_v4().simple()), |
| admin_password: None, |
| auto_start: false, |
| custom_mapping: std::collections::HashMap::new(), |
| request_timeout: default_request_timeout(), |
| enable_logging: true, |
| debug_logging: DebugLoggingConfig::default(), |
| upstream_proxy: UpstreamProxyConfig::default(), |
| zai: ZaiConfig::default(), |
| scheduling: crate::proxy::sticky_config::StickySessionConfig::default(), |
| experimental: ExperimentalConfig::default(), |
| security_monitor: SecurityMonitorConfig::default(), |
| preferred_account_id: None, |
| user_agent_override: None, |
| saved_user_agent: None, |
| thinking_budget: ThinkingBudgetConfig::default(), |
| global_system_prompt: GlobalSystemPromptConfig::default(), |
| proxy_pool: ProxyPoolConfig::default(), |
| image_thinking_mode: None, |
| } |
| } |
| } |
|
|
| fn default_request_timeout() -> u64 { |
| 120 |
| } |
|
|
| fn default_zai_base_url() -> String { |
| "https://api.z.ai/api/anthropic".to_string() |
| } |
|
|
| fn default_zai_opus_model() -> String { |
| "glm-4.7".to_string() |
| } |
|
|
| fn default_zai_sonnet_model() -> String { |
| "glm-4.7".to_string() |
| } |
|
|
| fn default_zai_haiku_model() -> String { |
| "glm-4.5-air".to_string() |
| } |
|
|
| impl ProxyConfig { |
| |
| |
| |
| pub fn get_bind_address(&self) -> &str { |
| if self.allow_lan_access { |
| "0.0.0.0" |
| } else { |
| "127.0.0.1" |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ProxyAuth { |
| pub username: String, |
| #[serde( |
| serialize_with = "crate::utils::crypto::serialize_password", |
| deserialize_with = "crate::utils::crypto::deserialize_password" |
| )] |
| pub password: String, |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ProxyEntry { |
| pub id: String, |
| pub name: String, |
| pub url: String, |
| pub auth: Option<ProxyAuth>, |
| pub enabled: bool, |
| pub priority: i32, |
| pub tags: Vec<String>, |
| pub max_accounts: Option<usize>, |
| pub health_check_url: Option<String>, |
| pub last_check_time: Option<i64>, |
| pub is_healthy: bool, |
| pub latency: Option<u64>, |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize)] |
| pub struct ProxyPoolConfig { |
| pub enabled: bool, |
| |
| pub proxies: Vec<ProxyEntry>, |
| pub health_check_interval: u64, |
| pub auto_failover: bool, |
| pub strategy: ProxySelectionStrategy, |
| |
| #[serde(default)] |
| pub account_bindings: HashMap<String, String>, |
| } |
|
|
| impl Default for ProxyPoolConfig { |
| fn default() -> Self { |
| Self { |
| enabled: false, |
| |
| proxies: Vec::new(), |
| health_check_interval: 300, |
| auto_failover: true, |
| strategy: ProxySelectionStrategy::Priority, |
| account_bindings: HashMap::new(), |
| } |
| } |
| } |
|
|
| |
| #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] |
| #[serde(rename_all = "snake_case")] |
| pub enum ProxySelectionStrategy { |
| |
| RoundRobin, |
| |
| Random, |
| |
| Priority, |
| |
| LeastConnections, |
| |
| WeightedRoundRobin, |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn test_normalize_proxy_url() { |
| |
| assert_eq!(normalize_proxy_url("http://127.0.0.1:7890"), "http://127.0.0.1:7890"); |
| assert_eq!(normalize_proxy_url("https://proxy.com"), "https://proxy.com"); |
| assert_eq!(normalize_proxy_url("socks5://127.0.0.1:1080"), "socks5://127.0.0.1:1080"); |
| assert_eq!(normalize_proxy_url("socks5h://127.0.0.1:1080"), "socks5h://127.0.0.1:1080"); |
|
|
| |
| assert_eq!(normalize_proxy_url("127.0.0.1:7890"), "http://127.0.0.1:7890"); |
| assert_eq!(normalize_proxy_url("localhost:1082"), "http://localhost:1082"); |
|
|
| |
| assert_eq!(normalize_proxy_url(""), ""); |
| assert_eq!(normalize_proxy_url(" "), ""); |
| } |
| } |
|
|