Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/alibaba_anolisa.json
T

1 line
64 KiB
JSON

{"repo": "alibaba/anolisa", "files": {"src/cosh-ng/crates/cosh-core/src/auth.rs": {"c0c514516f85288bd9d151a4a84a5b06a2b51693": {"t": 1785674198.3537018, "n": 31232, "h": "0dbbd903e3b15bca", "c": "use std::collections::HashMap;\nuse std::fmt;\n\nuse crate::config::{CoreConfig, ProviderConfig};\nuse crate::protocol::{AuthField, AuthProvider};\n\nmod exchange;\nmod validation;\n\npub(crate) use exchange::request_validated_auth;\npub(crate) use validation::AuthConfigValidationError;\nuse validation::{validate_auth_response, validate_base_url};\n\nconst DEFAULT_PLAN_MODEL: &str = \"qwen3.7-plus\";\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ServiceSite {\n China,\n International,\n}\n\nimpl ServiceSite {\n fn from_env() -> Self {\n std::env::var(\"COSH_SERVICE_SITE\")\n .ok()\n .and_then(|value| Self::parse(&value))\n .unwrap_or(Self::China)\n }\n\n fn parse(value: &str) -> Option<Self> {\n match value.trim().to_ascii_lowercase().as_str() {\n \"china\" | \"cn\" => Some(Self::China),\n \"international\" | \"intl\" | \"global\" => Some(Self::International),\n _ => None,\n }\n }\n}\n\nstruct PlanEndpoint {\n base_url: &'static str,\n api_key_url: &'static str,\n}\n\nfn coding_plan_endpoint(site: ServiceSite) -> PlanEndpoint {\n match site {\n ServiceSite::China => PlanEndpoint {\n base_url: \"https://coding.dashscope.aliyuncs.com/v1\",\n api_key_url:\n \"https://bailian.console.aliyun.com/?tab=coding-plan#/efm/coding-plan-detail\",\n },\n ServiceSite::International => PlanEndpoint {\n base_url: \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n api_key_url:\n \"https://modelstudio.console.alibabacloud.com/?tab=dashboard#/efm/coding_plan\",\n },\n }\n}\n\nfn token_plan_endpoint(site: ServiceSite) -> PlanEndpoint {\n match site {\n ServiceSite::China => PlanEndpoint {\n base_url: \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\",\n api_key_url:\n \"https://bailian.console.aliyun.com/?tab=plan#/efm/subscription/token-plan\",\n },\n ServiceSite::International => PlanEndpoint {\n base_url: \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\",\n api_key_url: \"https://www.alibabacloud.com/help/en/model-studio/token-plan-quickstart\",\n },\n }\n}\n\nfn plan_auth_provider(\n id: &str,\n label: &str,\n description: &str,\n description_zh_cn: &str,\n endpoint: PlanEndpoint,\n) -> AuthProvider {\n AuthProvider {\n id: id.to_string(),\n label: label.to_string(),\n description: Some(description.to_string()),\n description_zh_cn: Some(description_zh_cn.to_string()),\n fields: vec![\n AuthField {\n name: \"api_key\".to_string(),\n label: \"API Key\".to_string(),\n hint: Some(format!(\n \"Plan keys start with sk-sp-. Get yours: {}\",\n endpoint.api_key_url\n )),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(format!(\"Default: {DEFAULT_PLAN_MODEL}\")),\n secret: false,\n required: false,\n placeholder: Some(DEFAULT_PLAN_MODEL.to_string()),\n },\n ],\n builtin_base_url: Some(endpoint.base_url.to_string()),\n builtin_provider_type: id.to_string(),\n builtin_default_model: Some(DEFAULT_PLAN_MODEL.to_string()),\n }\n}\n\n/// Returns the builtin provider templates for the auth UI.\npub fn builtin_auth_providers() -> Vec<AuthProvider> {\n builtin_auth_providers_for_site(ServiceSite::from_env())\n}\n\nfn builtin_auth_providers_for_site(site: ServiceSite) -> Vec<AuthProvider> {\n vec![\n AuthProvider {\n id: \"aliyun\".to_string(),\n label: \"Aliyun Authentication\".to_string(),\n description: Some(\"Free with limited quota\".to_string()),\n description_zh_cn: Some(\"提供有限的免费额度\".to_string()),\n fields: vec![\n AuthField {\n name: \"access_key_id\".to_string(),\n label: \"Access Key ID\".to_string(),\n hint: Some(\"获取地址: https://ram.console.aliyun.com/manage/ak\".to_string()),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"access_key_secret\".to_string(),\n label: \"Access Key Secret\".to_string(),\n hint: None,\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(\"默认: qwen3.7-plus\".to_string()),\n secret: false,\n required: false,\n placeholder: Some(\"qwen3.7-plus\".to_string()),\n },\n ],\n builtin_base_url: None,\n builtin_provider_type: \"aliyun\".to_string(),\n builtin_default_model: Some(\"qwen3.7-plus\".to_string()),\n },\n plan_auth_provider(\n \"coding_plan\",\n \"Coding Plan\",\n \"For individual developers • Weekly quota included\",\n \"面向个人开发者 • 包含每周额度\",\n coding_plan_endpoint(site),\n ),\n plan_auth_provider(\n \"token_plan\",\n \"Token Plan\",\n \"For teams and companies • Usage-based billing with dedicated capacity\",\n \"面向团队和企业 • 按用量计费,提供专属容量\",\n token_plan_endpoint(site),\n ),\n AuthProvider {\n id: \"dashscope\".to_string(),\n label: \"DashScope (百炼)\".to_string(),\n description: Some(\"Connect with an existing Bailian API key\".to_string()),\n description_zh_cn: Some(\"使用现有的百炼 API Key\".to_string()),\n fields: vec![\n AuthField {\n name: \"api_key\".to_string(),\n label: \"API Key\".to_string(),\n hint: Some(\n \"获取地址: https://bailian.console.aliyun.com/?tab=model#/api-key\"\n .to_string(),\n ),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(\"默认: qwen3.7-plus, e.g. qwen3.7-max, deepseek-v4-pro\".to_string()),\n secret: false,\n required: false,\n placeholder: Some(\"qwen3.7-plus\".to_string()),\n },\n ],\n builtin_base_url: Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\".to_string()),\n builtin_provider_type: \"dashscope\".to_string(),\n builtin_default_model: Some(\"qwen3.7-plus\".to_string()),\n },\n AuthProvider {\n id: \"openai_compat\".to_string(),\n label: \"OpenAI Compatible\".to_string(),\n description: Some(\"Use an existing OpenAI-compatible Base URL and API key\".to_string()),\n description_zh_cn: Some(\"使用现有的 OpenAI 兼容 Base URL 和 API Key\".to_string()),\n fields: vec![\n AuthField {\n name: \"base_url\".to_string(),\n label: \"Base URL\".to_string(),\n hint: Some(\"例如: https://api.openai.com/v1\".to_string()),\n secret: false,\n required: true,\n placeholder: Some(\"https://api.openai.com/v1\".to_string()),\n },\n AuthField {\n name: \"api_key\".to_string(),\n label: \"API Key\".to_string(),\n hint: Some(\"sk-...\".to_string()),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(\"e.g. qwen3.7-max, deepseek-v4-pro\".to_string()),\n secret: false,\n required: true,\n placeholder: None,\n },\n ],\n builtin_base_url: None,\n builtin_provider_type: \"openai\".to_string(),\n builtin_default_model: None,\n },\n ]\n}\n\n/// Response from the auth flow.\npub struct AuthResponse {\n pub provider_id: String,\n pub provider_type: Option<String>,\n pub values: HashMap<String, String>,\n pub persist: bool,\n}\n\n/// Applies validated auth credentials and rebuilds provider settings.\n///\n/// # Errors\n///\n/// Returns an error when a required credential field is missing or a supplied\n/// base URL is malformed.\npub(crate) fn apply_auth_credentials(\n config: &mut CoreConfig,\n response: &AuthResponse,\n) -> Result<(), AuthConfigValidationError> {\n let template_id = response\n .provider_type\n .as_deref()\n .unwrap_or(response.provider_id.as_str());\n let template = builtin_auth_providers()\n .into_iter()\n .find(|provider| provider.id == template_id);\n match template.as_ref() {\n Some(template) => validate_auth_response(template, response)?,\n None => {\n let base_url = response\n .values\n .get(\"base_url\")\n .filter(|value| !value.trim().is_empty())\n .ok_or_else(|| {\n AuthConfigValidationError::MissingRequiredField(\"base_url\".to_string())\n })?;\n validate_base_url(base_url)?;\n }\n }\n\n let (base_url, provider_type, default_model) = match template {\n Some(template) => (\n response\n .values\n .get(\"base_url\")\n .cloned()\n .or(template.builtin_base_url)\n .unwrap_or_default(),\n template.builtin_provider_type,\n template.builtin_default_model,\n ),\n None => (\n response.values.get(\"base_url\").cloned().unwrap_or_default(),\n \"generic\".to_string(),\n None,\n ),\n };\n\n let user_model = response\n .values\n .get(\"model\")\n .filter(|m| !m.is_empty())\n .cloned();\n let final_model = user_model.or(default_model);\n\n let api_key = response.values.get(\"api_key\").cloned().unwrap_or_default();\n\n // Aliyun provider uses AK/SK instead of API key\n let auth_source = response.values.get(\"auth_source\").cloned();\n let is_ecs_ram_role = auth_source.as_deref() == Some(\"ecs_ram_role\");\n let access_key_id = if is_ecs_ram_role {\n None\n } else {\n response.values.get(\"access_key_id\").cloned()\n };\n let access_key_secret = if is_ecs_ram_role {\n None\n } else {\n response.values.get(\"access_key_secret\").cloned()\n };\n let security_token = if is_ecs_ram_role {\n None\n } else {\n response.values.get(\"security_token\").cloned()\n };\n\n // Preserve explicit_cache across auth refresh so a 401/403\n // re-auth does not silently reset the user's explicit cache preference\n // to None (which persist_config_to_dir would then omit).\n let existing_explicit_cache = config\n .ai\n .providers\n .get(&response.provider_id)\n .and_then(|p| p.explicit_cache);\n\n config.ai.active_provider = Some(response.provider_id.clone());\n let provider = ProviderConfig {\n provider_type: Some(provider_type),\n auth_source,\n base_url: Some(base_url),\n api_key: Some(api_key),\n model: final_model,\n extra_params: None,\n access_key_id,\n access_key_secret,\n security_token,\n explicit_cache: existing_explicit_cache,\n };\n config\n .ai\n .providers\n .insert(response.provider_id.clone(), provider.clone());\n if response.persist {\n config.user_ai.active_provider = Some(response.provider_id.clone());\n config\n .user_ai\n .providers\n .insert(response.provider_id.clone(), provider);\n }\n Ok(())\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub(crate) enum RemoveAuthProviderError {\n NotFound,\n NotEditable,\n}\n\nimpl fmt::Display for RemoveAuthProviderError {\n fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {\n match self {\n Self::NotFound => formatter.write_str(\"provider not found\"),\n Self::NotEditable => formatter.write_str(\"provider is not removable\"),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub(crate) struct RemovedAuthProvider {\n pub(crate) active_provider: Option<String>,\n}\n\npub(crate) fn remove_auth_provider(\n config: &mut CoreConfig,\n provider_id: &str,\n) -> Result<RemovedAuthProvider, RemoveAuthProviderError> {\n if !config.user_ai.providers.contains_key(provider_id) {\n return Err(if config.ai.providers.contains_key(provider_id) {\n RemoveAuthProviderError::NotEditable\n } else {\n RemoveAuthProviderError::NotFound\n });\n }\n\n let was_active = config.ai.active_provider.as_deref() == Some(provider_id);\n config.user_ai.providers.remove(provider_id);\n if config.user_ai.active_provider.as_deref() == Some(provider_id) {\n config.user_ai.active_provider = None;\n }\n\n if let Some(system_provider) = config.system_ai.providers.get(provider_id).cloned() {\n config\n .ai\n .providers\n .insert(provider_id.to_string(), system_provider);\n } else {\n config.ai.providers.remove(provider_id);\n }\n\n if was_active {\n let active_provider = config\n .system_ai\n .active_provider\n .clone()\n .filter(|fallback| config.ai.providers.contains_key(fallback));\n config.ai.active_provider = active_provider.clone();\n config.ai.active_model = active_provider.as_ref().and_then(|fallback| {\n config\n .ai\n .providers\n .get(fallback)\n .and_then(|provider| provider.model.clone())\n });\n }\n\n Ok(RemovedAuthProvider {\n active_provider: config.ai.active_provider.clone(),\n })\n}\n\n/// Check if an error string indicates an auth failure (401/403).\npub fn is_auth_error(error: &str) -> bool {\n error.contains(\"401\") || error.contains(\"403\") || error.contains(\"Unauthorized\")\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn builtin_providers_have_correct_ids() {\n let providers = builtin_auth_providers();\n assert_eq!(providers.len(), 5);\n assert_eq!(providers[0].id, \"aliyun\");\n assert_eq!(providers[1].id, \"coding_plan\");\n assert_eq!(providers[2].id, \"token_plan\");\n assert_eq!(providers[3].id, \"dashscope\");\n assert_eq!(providers[4].id, \"openai_compat\");\n }\n\n #[test]\n fn plan_endpoints_match_service_site() {\n let coding_cn = coding_plan_endpoint(ServiceSite::China);\n let coding_intl = coding_plan_endpoint(ServiceSite::International);\n let token_cn = token_plan_endpoint(ServiceSite::China);\n let token_intl = token_plan_endpoint(ServiceSite::International);\n\n assert_eq!(\n coding_cn.base_url,\n \"https://coding.dashscope.aliyuncs.com/v1\"\n );\n assert_eq!(\n coding_intl.base_url,\n \"https://coding-intl.dashscope.aliyuncs.com/v1\"\n );\n assert_eq!(\n token_cn.base_url,\n \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n );\n assert_eq!(\n token_intl.base_url,\n \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\"\n );\n }\n\n #[test]\n fn international_site_builds_plan_templates_with_international_urls() {\n let providers = builtin_auth_providers_for_site(ServiceSite::International);\n let coding_plan = providers\n .iter()\n .find(|provider| provider.id == \"coding_plan\")\n .unwrap();\n let token_plan = providers\n .iter()\n .find(|provider| provider.id == \"token_plan\")\n .unwrap();\n\n assert_eq!(\n coding_plan.builtin_base_url.as_deref(),\n Some(\"https://coding-intl.dashscope.aliyuncs.com/v1\")\n );\n assert_eq!(\n coding_plan.description_zh_cn.as_deref(),\n Some(\"面向个人开发者 • 包含每周额度\")\n );\n assert_eq!(\n coding_plan.description.as_deref(),\n Some(\"For individual developers • Weekly quota included\")\n );\n assert_eq!(\n token_plan.builtin_base_url.as_deref(),\n Some(\"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\")\n );\n assert_eq!(\n token_plan.description.as_deref(),\n Some(\"For teams and companies • Usage-based billing with dedicated capacity\")\n );\n assert_eq!(\n token_plan.description_zh_cn.as_deref(),\n Some(\"面向团队和企业 • 按用量计费,提供专属容量\")\n );\n }\n\n #[test]\n fn service_site_accepts_packaging_aliases() {\n assert_eq!(ServiceSite::parse(\"cn\"), Some(ServiceSite::China));\n assert_eq!(\n ServiceSite::parse(\"global\"),\n Some(ServiceSite::International)\n );\n assert_eq!(ServiceSite::parse(\"unknown\"), None);\n }\n\n #[test]\n fn dashscope_has_builtin_base_url() {\n let providers = builtin_auth_providers();\n let ds = providers\n .iter()\n .find(|provider| provider.id == \"dashscope\")\n .unwrap();\n assert!(ds.builtin_base_url.is_some());\n assert_eq!(ds.fields.len(), 2);\n assert_eq!(ds.fields[0].name, \"api_key\");\n assert_eq!(ds.fields[1].name, \"model\");\n }\n\n #[test]\n fn openai_compat_has_no_builtin_base_url() {\n let providers = builtin_auth_providers();\n let oc = providers\n .iter()\n .find(|provider| provider.id == \"openai_compat\")\n .unwrap();\n assert!(oc.builtin_base_url.is_none());\n assert_eq!(oc.fields.len(), 3);\n assert_eq!(oc.fields[0].name, \"base_url\");\n assert_eq!(oc.fields[1].name, \"api_key\");\n assert_eq!(oc.fields[2].name, \"model\");\n }\n\n #[test]\n fn apply_dashscope_credentials() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"dashscope\".to_string(),\n provider_type: None,\n values: HashMap::from([(\"api_key\".to_string(), \"sk-test123\".to_string())]),\n persist: true,\n };\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert_eq!(config.ai.active_provider.as_deref(), Some(\"dashscope\"));\n let p = config.ai.providers.get(\"dashscope\").unwrap();\n assert_eq!(p.api_key.as_deref(), Some(\"sk-test123\"));\n assert_eq!(\n p.base_url.as_deref(),\n Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n );\n assert_eq!(p.provider_type.as_deref(), Some(\"dashscope\"));\n assert_eq!(p.model.as_deref(), Some(\"qwen3.7-plus\"));\n }\n\n #[test]\n fn apply_coding_plan_credentials_uses_preset_endpoint() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"coding-plan\".to_string(),\n provider_type: Some(\"coding_plan\".to_string()),\n values: HashMap::from([(\"api_key\".to_string(), \"sk-sp-test\".to_string())]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n let provider = config.ai.providers.get(\"coding-plan\").unwrap();\n assert_eq!(provider.provider_type.as_deref(), Some(\"coding_plan\"));\n assert_eq!(\n provider.base_url.as_deref(),\n Some(\"https://coding.dashscope.aliyuncs.com/v1\")\n );\n assert_eq!(provider.model.as_deref(), Some(DEFAULT_PLAN_MODEL));\n }\n\n #[test]\n fn apply_openai_compat_credentials() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"openai_compat\".to_string(),\n provider_type: None,\n values: HashMap::from([\n (\n \"base_url\".to_string(),\n \"https://api.openai.com/v1\".to_string(),\n ),\n (\"api_key\".to_string(), \"sk-openai\".to_string()),\n (\"model\".to_string(), \"gpt-4o\".to_string()),\n ]),\n persist: false,\n };\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert_eq!(config.ai.active_provider.as_deref(), Some(\"openai_compat\"));\n let p = config.ai.providers.get(\"openai_compat\").unwrap();\n assert_eq!(p.api_key.as_deref(), Some(\"sk-openai\"));\n assert_eq!(p.base_url.as_deref(), Some(\"https://api.openai.com/v1\"));\n assert_eq!(p.provider_type.as_deref(), Some(\"openai\"));\n }\n\n #[test]\n fn apply_credentials_uses_provider_id_as_config_key_and_type_as_template() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"qwen-prod\".to_string(),\n provider_type: Some(\"dashscope\".to_string()),\n values: HashMap::from([(\"api_key\".to_string(), \"sk-prod\".to_string())]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert_eq!(config.ai.active_provider.as_deref(), Some(\"qwen-prod\"));\n assert!(config.ai.providers.contains_key(\"qwen-prod\"));\n assert!(!config.ai.providers.contains_key(\"dashscope\"));\n let provider = config.ai.providers.get(\"qwen-prod\").unwrap();\n assert_eq!(provider.provider_type.as_deref(), Some(\"dashscope\"));\n assert_eq!(provider.api_key.as_deref(), Some(\"sk-prod\"));\n assert_eq!(\n provider.base_url.as_deref(),\n Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n );\n }\n\n #[test]\n fn apply_unknown_provider_preserves_generic_fallback() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"custom-provider\".to_string(),\n provider_type: Some(\"custom-provider\".to_string()),\n values: HashMap::from([\n (\n \"base_url\".to_string(),\n \"https://api.example.com/v1\".to_string(),\n ),\n (\"api_key\".to_string(), \"sk-custom\".to_string()),\n (\"model\".to_string(), \"custom-model\".to_string()),\n ]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n let provider = config.ai.providers.get(\"custom-provider\").unwrap();\n assert_eq!(provider.provider_type.as_deref(), Some(\"generic\"));\n assert_eq!(\n provider.base_url.as_deref(),\n Some(\"https://api.example.com/v1\")\n );\n assert_eq!(provider.model.as_deref(), Some(\"custom-model\"));\n }\n\n #[test]\n fn apply_unknown_provider_rejects_invalid_base_url() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"custom-provider\".to_string(),\n provider_type: Some(\"custom-provider\".to_string()),\n values: HashMap::from([(\n \"base_url\".to_string(),\n \"file:///tmp/custom-provider\".to_string(),\n )]),\n persist: true,\n };\n\n assert_eq!(\n apply_auth_credentials(&mut config, &response),\n Err(AuthConfigValidationError::InvalidBaseUrl)\n );\n assert!(config.ai.providers.is_empty());\n assert!(config.user_ai.providers.is_empty());\n }\n\n #[test]\n fn apply_unknown_provider_requires_base_url() {\n for values in [\n HashMap::new(),\n HashMap::from([(\"base_url\".to_string(), \" \".to_string())]),\n ] {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"custom-provider\".to_string(),\n provider_type: Some(\"custom-provider\".to_string()),\n values,\n persist: true,\n };\n\n assert_eq!(\n apply_auth_credentials(&mut config, &response),\n Err(AuthConfigValidationError::MissingRequiredField(\n \"base_url\".to_string()\n ))\n );\n assert!(config.ai.providers.is_empty());\n assert!(config.user_ai.providers.is_empty());\n }\n }\n\n #[test]\n fn persisted_auth_credentials_update_user_layer_only_for_persistence() {\n let mut config = CoreConfig::default();\n config.ai.providers.insert(\n \"system-provider\".to_string(),\n ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n api_key: Some(\"sk-system\".to_string()),\n ..Default::default()\n },\n );\n let response = AuthResponse {\n provider_id: \"user-provider\".to_string(),\n provider_type: Some(\"dashscope\".to_string()),\n values: HashMap::from([(\"api_key\".to_string(), \"sk-user\".to_string())]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert!(config.ai.providers.contains_key(\"system-provider\"));\n assert!(config.ai.providers.contains_key(\"user-provider\"));\n assert!(!config.user_ai.providers.contains_key(\"system-provider\"));\n assert_eq!(\n config.user_ai.active_provider.as_deref(),\n Some(\"user-provider\")\n );\n assert_eq!(\n config\n .user_ai\n .providers\n .get(\"user-provider\")\n .and_then(|provider| provider.api_key.as_deref()),\n Some(\"sk-user\")\n );\n }\n\n #[test]\n fn auth_refresh_preserves_explicit_cache() {\n let mut config = CoreConfig::default();\n config.ai.providers.insert(\n \"dashscope\".to_string(),\n ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n base_url: Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\".to_string()),\n api_key: Some(\"sk-old\".to_string()),\n model: Some(\"qwen3.7-plus\".to_string()),\n explicit_cache: Some(true),\n ..Default::default()\n },\n );\n\n // Simulate 401/403 re-auth: same provider_id, new api_key\n let response = AuthResponse {\n provider_id: \"dashscope\".to_string(),\n provider_type: None,\n values: HashMap::from([(\"api_key\".to_string(), \"sk-new\".to_string())]),\n persist: true,\n };\n apply_auth_credentials(&mut config, &response).unwrap();\n\n let p = config.ai.providers.get(\"dashscope\").unwrap();\n assert_eq!(p.api_key.as_deref(), Some(\"sk-new\"));\n // explicit_cache must survive re-auth\n assert_eq!(p.explicit_cache, Some(true));\n }\n\n #[test]\n fn remove_auth_provider_drops_user_credentials_and_active_selection() {\n let provider = ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n api_key: Some(\"sk-user\".to_string()),\n model: Some(\"user-model\".to_string()),\n ..Default::default()\n };\n let mut config = CoreConfig::default();\n config.ai.active_provider = Some(\"user-provider\".to_string());\n config.ai.active_model = Some(\"user-model\".to_string());\n config\n .ai\n .providers\n .insert(\"user-provider\".to_string(), provider.clone());\n config.user_ai.active_provider = Some(\"user-provider\".to_string());\n config\n .user_ai\n .providers\n .insert(\"user-provider\".to_string(), provider);\n\n let removed = remove_auth_provider(&mut config, \"user-provider\").unwrap();\n\n assert_eq!(removed.active_provider, None);\n assert_eq!(config.ai.active_provider, None);\n assert_eq!(config.ai.active_model, None);\n assert!(!config.ai.providers.contains_key(\"user-provider\"));\n assert!(!config.user_ai.providers.contains_key(\"user-provider\"));\n }\n\n #[test]\n fn remove_auth_provider_reveals_system_provider_with_same_id() {\n let system_provider = ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n api_key: Some(\"sk-system\".to_string()),\n model: Some(\"system-model\".to_string()),\n ..Default::default()\n };\n let user_provider = ProviderConfig {\n provider_type: Some(\"openai\".to_string()),\n api_key: Some(\"sk-user\".to_string()),\n model: Some(\"user-model\".to_string()),\n ..Default::default()\n };\n let mut config = CoreConfig::default();\n config.ai.active_provider = Some(\"shared\".to_string());\n config\n .ai\n .providers\n .insert(\"shared\".to_string(), user_provider.clone());\n config\n .system_ai\n .providers\n .insert(\"shared\".to_string(), system_provider);\n config.system_ai.active_provider = Some(\"shared\".to_string());\n config.user_ai.active_provider = Some(\"shared\".to_string());\n config\n .user_ai\n .providers\n .insert(\"shared\".to_string(), user_provider);\n\n let removed = remove_auth_provider(&mut config, \"shared\").unwrap();\n\n assert_eq!(removed.active_provider.as_deref(), Some(\"shared\"));\n assert_eq!(\n config\n .ai\n .providers\n .get(\"shared\")\n .and_then(|provider| provider.api_key.as_deref()),\n Some(\"sk-system\")\n );\n assert_eq!(config.ai.active_model.as_deref(), Some(\"system-model\"));\n assert_eq!(config.user_ai.active_provider, None);\n }\n\n #[test]\n fn remove_auth_provider_rejects_system_provider() {\n let mut config = CoreConfig::default();\n config\n .ai\n .providers\n .insert(\"system-provider\".to_string(), ProviderConfig::default());\n\n assert_eq!(\n remove_auth_provider(&mut config, \"system-provider\"),\n Err(RemoveAuthProviderError::NotEditable)\n );\n }\n\n #[test]\n fn is_auth_error_detects_401() {\n assert!(is_auth_error(\"API error 401: invalid api key\"));\n assert!(is_auth_error(\"HTTP 403 Forbidden\"));\n assert!(is_auth_error(\"Unauthorized access\"));\n assert!(!is_auth_error(\"API error 500: internal server error\"));\n }\n}\n"}, "068e6cc18a4823cf94d8a06362806ff79399b32e": {"t": 1785846847.3216355, "n": 31232, "h": "0dbbd903e3b15bca", "c": "use std::collections::HashMap;\nuse std::fmt;\n\nuse crate::config::{CoreConfig, ProviderConfig};\nuse crate::protocol::{AuthField, AuthProvider};\n\nmod exchange;\nmod validation;\n\npub(crate) use exchange::request_validated_auth;\npub(crate) use validation::AuthConfigValidationError;\nuse validation::{validate_auth_response, validate_base_url};\n\nconst DEFAULT_PLAN_MODEL: &str = \"qwen3.7-plus\";\n\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\nenum ServiceSite {\n China,\n International,\n}\n\nimpl ServiceSite {\n fn from_env() -> Self {\n std::env::var(\"COSH_SERVICE_SITE\")\n .ok()\n .and_then(|value| Self::parse(&value))\n .unwrap_or(Self::China)\n }\n\n fn parse(value: &str) -> Option<Self> {\n match value.trim().to_ascii_lowercase().as_str() {\n \"china\" | \"cn\" => Some(Self::China),\n \"international\" | \"intl\" | \"global\" => Some(Self::International),\n _ => None,\n }\n }\n}\n\nstruct PlanEndpoint {\n base_url: &'static str,\n api_key_url: &'static str,\n}\n\nfn coding_plan_endpoint(site: ServiceSite) -> PlanEndpoint {\n match site {\n ServiceSite::China => PlanEndpoint {\n base_url: \"https://coding.dashscope.aliyuncs.com/v1\",\n api_key_url:\n \"https://bailian.console.aliyun.com/?tab=coding-plan#/efm/coding-plan-detail\",\n },\n ServiceSite::International => PlanEndpoint {\n base_url: \"https://coding-intl.dashscope.aliyuncs.com/v1\",\n api_key_url:\n \"https://modelstudio.console.alibabacloud.com/?tab=dashboard#/efm/coding_plan\",\n },\n }\n}\n\nfn token_plan_endpoint(site: ServiceSite) -> PlanEndpoint {\n match site {\n ServiceSite::China => PlanEndpoint {\n base_url: \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\",\n api_key_url:\n \"https://bailian.console.aliyun.com/?tab=plan#/efm/subscription/token-plan\",\n },\n ServiceSite::International => PlanEndpoint {\n base_url: \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\",\n api_key_url: \"https://www.alibabacloud.com/help/en/model-studio/token-plan-quickstart\",\n },\n }\n}\n\nfn plan_auth_provider(\n id: &str,\n label: &str,\n description: &str,\n description_zh_cn: &str,\n endpoint: PlanEndpoint,\n) -> AuthProvider {\n AuthProvider {\n id: id.to_string(),\n label: label.to_string(),\n description: Some(description.to_string()),\n description_zh_cn: Some(description_zh_cn.to_string()),\n fields: vec![\n AuthField {\n name: \"api_key\".to_string(),\n label: \"API Key\".to_string(),\n hint: Some(format!(\n \"Plan keys start with sk-sp-. Get yours: {}\",\n endpoint.api_key_url\n )),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(format!(\"Default: {DEFAULT_PLAN_MODEL}\")),\n secret: false,\n required: false,\n placeholder: Some(DEFAULT_PLAN_MODEL.to_string()),\n },\n ],\n builtin_base_url: Some(endpoint.base_url.to_string()),\n builtin_provider_type: id.to_string(),\n builtin_default_model: Some(DEFAULT_PLAN_MODEL.to_string()),\n }\n}\n\n/// Returns the builtin provider templates for the auth UI.\npub fn builtin_auth_providers() -> Vec<AuthProvider> {\n builtin_auth_providers_for_site(ServiceSite::from_env())\n}\n\nfn builtin_auth_providers_for_site(site: ServiceSite) -> Vec<AuthProvider> {\n vec![\n AuthProvider {\n id: \"aliyun\".to_string(),\n label: \"Aliyun Authentication\".to_string(),\n description: Some(\"Free with limited quota\".to_string()),\n description_zh_cn: Some(\"提供有限的免费额度\".to_string()),\n fields: vec![\n AuthField {\n name: \"access_key_id\".to_string(),\n label: \"Access Key ID\".to_string(),\n hint: Some(\"获取地址: https://ram.console.aliyun.com/manage/ak\".to_string()),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"access_key_secret\".to_string(),\n label: \"Access Key Secret\".to_string(),\n hint: None,\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(\"默认: qwen3.7-plus\".to_string()),\n secret: false,\n required: false,\n placeholder: Some(\"qwen3.7-plus\".to_string()),\n },\n ],\n builtin_base_url: None,\n builtin_provider_type: \"aliyun\".to_string(),\n builtin_default_model: Some(\"qwen3.7-plus\".to_string()),\n },\n plan_auth_provider(\n \"coding_plan\",\n \"Coding Plan\",\n \"For individual developers • Weekly quota included\",\n \"面向个人开发者 • 包含每周额度\",\n coding_plan_endpoint(site),\n ),\n plan_auth_provider(\n \"token_plan\",\n \"Token Plan\",\n \"For teams and companies • Usage-based billing with dedicated capacity\",\n \"面向团队和企业 • 按用量计费,提供专属容量\",\n token_plan_endpoint(site),\n ),\n AuthProvider {\n id: \"dashscope\".to_string(),\n label: \"DashScope (百炼)\".to_string(),\n description: Some(\"Connect with an existing Bailian API key\".to_string()),\n description_zh_cn: Some(\"使用现有的百炼 API Key\".to_string()),\n fields: vec![\n AuthField {\n name: \"api_key\".to_string(),\n label: \"API Key\".to_string(),\n hint: Some(\n \"获取地址: https://bailian.console.aliyun.com/?tab=model#/api-key\"\n .to_string(),\n ),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(\"默认: qwen3.7-plus, e.g. qwen3.7-max, deepseek-v4-pro\".to_string()),\n secret: false,\n required: false,\n placeholder: Some(\"qwen3.7-plus\".to_string()),\n },\n ],\n builtin_base_url: Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\".to_string()),\n builtin_provider_type: \"dashscope\".to_string(),\n builtin_default_model: Some(\"qwen3.7-plus\".to_string()),\n },\n AuthProvider {\n id: \"openai_compat\".to_string(),\n label: \"OpenAI Compatible\".to_string(),\n description: Some(\"Use an existing OpenAI-compatible Base URL and API key\".to_string()),\n description_zh_cn: Some(\"使用现有的 OpenAI 兼容 Base URL 和 API Key\".to_string()),\n fields: vec![\n AuthField {\n name: \"base_url\".to_string(),\n label: \"Base URL\".to_string(),\n hint: Some(\"例如: https://api.openai.com/v1\".to_string()),\n secret: false,\n required: true,\n placeholder: Some(\"https://api.openai.com/v1\".to_string()),\n },\n AuthField {\n name: \"api_key\".to_string(),\n label: \"API Key\".to_string(),\n hint: Some(\"sk-...\".to_string()),\n secret: true,\n required: true,\n placeholder: None,\n },\n AuthField {\n name: \"model\".to_string(),\n label: \"Model\".to_string(),\n hint: Some(\"e.g. qwen3.7-max, deepseek-v4-pro\".to_string()),\n secret: false,\n required: true,\n placeholder: None,\n },\n ],\n builtin_base_url: None,\n builtin_provider_type: \"openai\".to_string(),\n builtin_default_model: None,\n },\n ]\n}\n\n/// Response from the auth flow.\npub struct AuthResponse {\n pub provider_id: String,\n pub provider_type: Option<String>,\n pub values: HashMap<String, String>,\n pub persist: bool,\n}\n\n/// Applies validated auth credentials and rebuilds provider settings.\n///\n/// # Errors\n///\n/// Returns an error when a required credential field is missing or a supplied\n/// base URL is malformed.\npub(crate) fn apply_auth_credentials(\n config: &mut CoreConfig,\n response: &AuthResponse,\n) -> Result<(), AuthConfigValidationError> {\n let template_id = response\n .provider_type\n .as_deref()\n .unwrap_or(response.provider_id.as_str());\n let template = builtin_auth_providers()\n .into_iter()\n .find(|provider| provider.id == template_id);\n match template.as_ref() {\n Some(template) => validate_auth_response(template, response)?,\n None => {\n let base_url = response\n .values\n .get(\"base_url\")\n .filter(|value| !value.trim().is_empty())\n .ok_or_else(|| {\n AuthConfigValidationError::MissingRequiredField(\"base_url\".to_string())\n })?;\n validate_base_url(base_url)?;\n }\n }\n\n let (base_url, provider_type, default_model) = match template {\n Some(template) => (\n response\n .values\n .get(\"base_url\")\n .cloned()\n .or(template.builtin_base_url)\n .unwrap_or_default(),\n template.builtin_provider_type,\n template.builtin_default_model,\n ),\n None => (\n response.values.get(\"base_url\").cloned().unwrap_or_default(),\n \"generic\".to_string(),\n None,\n ),\n };\n\n let user_model = response\n .values\n .get(\"model\")\n .filter(|m| !m.is_empty())\n .cloned();\n let final_model = user_model.or(default_model);\n\n let api_key = response.values.get(\"api_key\").cloned().unwrap_or_default();\n\n // Aliyun provider uses AK/SK instead of API key\n let auth_source = response.values.get(\"auth_source\").cloned();\n let is_ecs_ram_role = auth_source.as_deref() == Some(\"ecs_ram_role\");\n let access_key_id = if is_ecs_ram_role {\n None\n } else {\n response.values.get(\"access_key_id\").cloned()\n };\n let access_key_secret = if is_ecs_ram_role {\n None\n } else {\n response.values.get(\"access_key_secret\").cloned()\n };\n let security_token = if is_ecs_ram_role {\n None\n } else {\n response.values.get(\"security_token\").cloned()\n };\n\n // Preserve explicit_cache across auth refresh so a 401/403\n // re-auth does not silently reset the user's explicit cache preference\n // to None (which persist_config_to_dir would then omit).\n let existing_explicit_cache = config\n .ai\n .providers\n .get(&response.provider_id)\n .and_then(|p| p.explicit_cache);\n\n config.ai.active_provider = Some(response.provider_id.clone());\n let provider = ProviderConfig {\n provider_type: Some(provider_type),\n auth_source,\n base_url: Some(base_url),\n api_key: Some(api_key),\n model: final_model,\n extra_params: None,\n access_key_id,\n access_key_secret,\n security_token,\n explicit_cache: existing_explicit_cache,\n };\n config\n .ai\n .providers\n .insert(response.provider_id.clone(), provider.clone());\n if response.persist {\n config.user_ai.active_provider = Some(response.provider_id.clone());\n config\n .user_ai\n .providers\n .insert(response.provider_id.clone(), provider);\n }\n Ok(())\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub(crate) enum RemoveAuthProviderError {\n NotFound,\n NotEditable,\n}\n\nimpl fmt::Display for RemoveAuthProviderError {\n fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {\n match self {\n Self::NotFound => formatter.write_str(\"provider not found\"),\n Self::NotEditable => formatter.write_str(\"provider is not removable\"),\n }\n }\n}\n\n#[derive(Debug, Clone, PartialEq, Eq)]\npub(crate) struct RemovedAuthProvider {\n pub(crate) active_provider: Option<String>,\n}\n\npub(crate) fn remove_auth_provider(\n config: &mut CoreConfig,\n provider_id: &str,\n) -> Result<RemovedAuthProvider, RemoveAuthProviderError> {\n if !config.user_ai.providers.contains_key(provider_id) {\n return Err(if config.ai.providers.contains_key(provider_id) {\n RemoveAuthProviderError::NotEditable\n } else {\n RemoveAuthProviderError::NotFound\n });\n }\n\n let was_active = config.ai.active_provider.as_deref() == Some(provider_id);\n config.user_ai.providers.remove(provider_id);\n if config.user_ai.active_provider.as_deref() == Some(provider_id) {\n config.user_ai.active_provider = None;\n }\n\n if let Some(system_provider) = config.system_ai.providers.get(provider_id).cloned() {\n config\n .ai\n .providers\n .insert(provider_id.to_string(), system_provider);\n } else {\n config.ai.providers.remove(provider_id);\n }\n\n if was_active {\n let active_provider = config\n .system_ai\n .active_provider\n .clone()\n .filter(|fallback| config.ai.providers.contains_key(fallback));\n config.ai.active_provider = active_provider.clone();\n config.ai.active_model = active_provider.as_ref().and_then(|fallback| {\n config\n .ai\n .providers\n .get(fallback)\n .and_then(|provider| provider.model.clone())\n });\n }\n\n Ok(RemovedAuthProvider {\n active_provider: config.ai.active_provider.clone(),\n })\n}\n\n/// Check if an error string indicates an auth failure (401/403).\npub fn is_auth_error(error: &str) -> bool {\n error.contains(\"401\") || error.contains(\"403\") || error.contains(\"Unauthorized\")\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn builtin_providers_have_correct_ids() {\n let providers = builtin_auth_providers();\n assert_eq!(providers.len(), 5);\n assert_eq!(providers[0].id, \"aliyun\");\n assert_eq!(providers[1].id, \"coding_plan\");\n assert_eq!(providers[2].id, \"token_plan\");\n assert_eq!(providers[3].id, \"dashscope\");\n assert_eq!(providers[4].id, \"openai_compat\");\n }\n\n #[test]\n fn plan_endpoints_match_service_site() {\n let coding_cn = coding_plan_endpoint(ServiceSite::China);\n let coding_intl = coding_plan_endpoint(ServiceSite::International);\n let token_cn = token_plan_endpoint(ServiceSite::China);\n let token_intl = token_plan_endpoint(ServiceSite::International);\n\n assert_eq!(\n coding_cn.base_url,\n \"https://coding.dashscope.aliyuncs.com/v1\"\n );\n assert_eq!(\n coding_intl.base_url,\n \"https://coding-intl.dashscope.aliyuncs.com/v1\"\n );\n assert_eq!(\n token_cn.base_url,\n \"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1\"\n );\n assert_eq!(\n token_intl.base_url,\n \"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\"\n );\n }\n\n #[test]\n fn international_site_builds_plan_templates_with_international_urls() {\n let providers = builtin_auth_providers_for_site(ServiceSite::International);\n let coding_plan = providers\n .iter()\n .find(|provider| provider.id == \"coding_plan\")\n .unwrap();\n let token_plan = providers\n .iter()\n .find(|provider| provider.id == \"token_plan\")\n .unwrap();\n\n assert_eq!(\n coding_plan.builtin_base_url.as_deref(),\n Some(\"https://coding-intl.dashscope.aliyuncs.com/v1\")\n );\n assert_eq!(\n coding_plan.description_zh_cn.as_deref(),\n Some(\"面向个人开发者 • 包含每周额度\")\n );\n assert_eq!(\n coding_plan.description.as_deref(),\n Some(\"For individual developers • Weekly quota included\")\n );\n assert_eq!(\n token_plan.builtin_base_url.as_deref(),\n Some(\"https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1\")\n );\n assert_eq!(\n token_plan.description.as_deref(),\n Some(\"For teams and companies • Usage-based billing with dedicated capacity\")\n );\n assert_eq!(\n token_plan.description_zh_cn.as_deref(),\n Some(\"面向团队和企业 • 按用量计费,提供专属容量\")\n );\n }\n\n #[test]\n fn service_site_accepts_packaging_aliases() {\n assert_eq!(ServiceSite::parse(\"cn\"), Some(ServiceSite::China));\n assert_eq!(\n ServiceSite::parse(\"global\"),\n Some(ServiceSite::International)\n );\n assert_eq!(ServiceSite::parse(\"unknown\"), None);\n }\n\n #[test]\n fn dashscope_has_builtin_base_url() {\n let providers = builtin_auth_providers();\n let ds = providers\n .iter()\n .find(|provider| provider.id == \"dashscope\")\n .unwrap();\n assert!(ds.builtin_base_url.is_some());\n assert_eq!(ds.fields.len(), 2);\n assert_eq!(ds.fields[0].name, \"api_key\");\n assert_eq!(ds.fields[1].name, \"model\");\n }\n\n #[test]\n fn openai_compat_has_no_builtin_base_url() {\n let providers = builtin_auth_providers();\n let oc = providers\n .iter()\n .find(|provider| provider.id == \"openai_compat\")\n .unwrap();\n assert!(oc.builtin_base_url.is_none());\n assert_eq!(oc.fields.len(), 3);\n assert_eq!(oc.fields[0].name, \"base_url\");\n assert_eq!(oc.fields[1].name, \"api_key\");\n assert_eq!(oc.fields[2].name, \"model\");\n }\n\n #[test]\n fn apply_dashscope_credentials() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"dashscope\".to_string(),\n provider_type: None,\n values: HashMap::from([(\"api_key\".to_string(), \"sk-test123\".to_string())]),\n persist: true,\n };\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert_eq!(config.ai.active_provider.as_deref(), Some(\"dashscope\"));\n let p = config.ai.providers.get(\"dashscope\").unwrap();\n assert_eq!(p.api_key.as_deref(), Some(\"sk-test123\"));\n assert_eq!(\n p.base_url.as_deref(),\n Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n );\n assert_eq!(p.provider_type.as_deref(), Some(\"dashscope\"));\n assert_eq!(p.model.as_deref(), Some(\"qwen3.7-plus\"));\n }\n\n #[test]\n fn apply_coding_plan_credentials_uses_preset_endpoint() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"coding-plan\".to_string(),\n provider_type: Some(\"coding_plan\".to_string()),\n values: HashMap::from([(\"api_key\".to_string(), \"sk-sp-test\".to_string())]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n let provider = config.ai.providers.get(\"coding-plan\").unwrap();\n assert_eq!(provider.provider_type.as_deref(), Some(\"coding_plan\"));\n assert_eq!(\n provider.base_url.as_deref(),\n Some(\"https://coding.dashscope.aliyuncs.com/v1\")\n );\n assert_eq!(provider.model.as_deref(), Some(DEFAULT_PLAN_MODEL));\n }\n\n #[test]\n fn apply_openai_compat_credentials() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"openai_compat\".to_string(),\n provider_type: None,\n values: HashMap::from([\n (\n \"base_url\".to_string(),\n \"https://api.openai.com/v1\".to_string(),\n ),\n (\"api_key\".to_string(), \"sk-openai\".to_string()),\n (\"model\".to_string(), \"gpt-4o\".to_string()),\n ]),\n persist: false,\n };\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert_eq!(config.ai.active_provider.as_deref(), Some(\"openai_compat\"));\n let p = config.ai.providers.get(\"openai_compat\").unwrap();\n assert_eq!(p.api_key.as_deref(), Some(\"sk-openai\"));\n assert_eq!(p.base_url.as_deref(), Some(\"https://api.openai.com/v1\"));\n assert_eq!(p.provider_type.as_deref(), Some(\"openai\"));\n }\n\n #[test]\n fn apply_credentials_uses_provider_id_as_config_key_and_type_as_template() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"qwen-prod\".to_string(),\n provider_type: Some(\"dashscope\".to_string()),\n values: HashMap::from([(\"api_key\".to_string(), \"sk-prod\".to_string())]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert_eq!(config.ai.active_provider.as_deref(), Some(\"qwen-prod\"));\n assert!(config.ai.providers.contains_key(\"qwen-prod\"));\n assert!(!config.ai.providers.contains_key(\"dashscope\"));\n let provider = config.ai.providers.get(\"qwen-prod\").unwrap();\n assert_eq!(provider.provider_type.as_deref(), Some(\"dashscope\"));\n assert_eq!(provider.api_key.as_deref(), Some(\"sk-prod\"));\n assert_eq!(\n provider.base_url.as_deref(),\n Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n );\n }\n\n #[test]\n fn apply_unknown_provider_preserves_generic_fallback() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"custom-provider\".to_string(),\n provider_type: Some(\"custom-provider\".to_string()),\n values: HashMap::from([\n (\n \"base_url\".to_string(),\n \"https://api.example.com/v1\".to_string(),\n ),\n (\"api_key\".to_string(), \"sk-custom\".to_string()),\n (\"model\".to_string(), \"custom-model\".to_string()),\n ]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n let provider = config.ai.providers.get(\"custom-provider\").unwrap();\n assert_eq!(provider.provider_type.as_deref(), Some(\"generic\"));\n assert_eq!(\n provider.base_url.as_deref(),\n Some(\"https://api.example.com/v1\")\n );\n assert_eq!(provider.model.as_deref(), Some(\"custom-model\"));\n }\n\n #[test]\n fn apply_unknown_provider_rejects_invalid_base_url() {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"custom-provider\".to_string(),\n provider_type: Some(\"custom-provider\".to_string()),\n values: HashMap::from([(\n \"base_url\".to_string(),\n \"file:///tmp/custom-provider\".to_string(),\n )]),\n persist: true,\n };\n\n assert_eq!(\n apply_auth_credentials(&mut config, &response),\n Err(AuthConfigValidationError::InvalidBaseUrl)\n );\n assert!(config.ai.providers.is_empty());\n assert!(config.user_ai.providers.is_empty());\n }\n\n #[test]\n fn apply_unknown_provider_requires_base_url() {\n for values in [\n HashMap::new(),\n HashMap::from([(\"base_url\".to_string(), \" \".to_string())]),\n ] {\n let mut config = CoreConfig::default();\n let response = AuthResponse {\n provider_id: \"custom-provider\".to_string(),\n provider_type: Some(\"custom-provider\".to_string()),\n values,\n persist: true,\n };\n\n assert_eq!(\n apply_auth_credentials(&mut config, &response),\n Err(AuthConfigValidationError::MissingRequiredField(\n \"base_url\".to_string()\n ))\n );\n assert!(config.ai.providers.is_empty());\n assert!(config.user_ai.providers.is_empty());\n }\n }\n\n #[test]\n fn persisted_auth_credentials_update_user_layer_only_for_persistence() {\n let mut config = CoreConfig::default();\n config.ai.providers.insert(\n \"system-provider\".to_string(),\n ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n api_key: Some(\"sk-system\".to_string()),\n ..Default::default()\n },\n );\n let response = AuthResponse {\n provider_id: \"user-provider\".to_string(),\n provider_type: Some(\"dashscope\".to_string()),\n values: HashMap::from([(\"api_key\".to_string(), \"sk-user\".to_string())]),\n persist: true,\n };\n\n apply_auth_credentials(&mut config, &response).unwrap();\n\n assert!(config.ai.providers.contains_key(\"system-provider\"));\n assert!(config.ai.providers.contains_key(\"user-provider\"));\n assert!(!config.user_ai.providers.contains_key(\"system-provider\"));\n assert_eq!(\n config.user_ai.active_provider.as_deref(),\n Some(\"user-provider\")\n );\n assert_eq!(\n config\n .user_ai\n .providers\n .get(\"user-provider\")\n .and_then(|provider| provider.api_key.as_deref()),\n Some(\"sk-user\")\n );\n }\n\n #[test]\n fn auth_refresh_preserves_explicit_cache() {\n let mut config = CoreConfig::default();\n config.ai.providers.insert(\n \"dashscope\".to_string(),\n ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n base_url: Some(\"https://dashscope.aliyuncs.com/compatible-mode/v1\".to_string()),\n api_key: Some(\"sk-old\".to_string()),\n model: Some(\"qwen3.7-plus\".to_string()),\n explicit_cache: Some(true),\n ..Default::default()\n },\n );\n\n // Simulate 401/403 re-auth: same provider_id, new api_key\n let response = AuthResponse {\n provider_id: \"dashscope\".to_string(),\n provider_type: None,\n values: HashMap::from([(\"api_key\".to_string(), \"sk-new\".to_string())]),\n persist: true,\n };\n apply_auth_credentials(&mut config, &response).unwrap();\n\n let p = config.ai.providers.get(\"dashscope\").unwrap();\n assert_eq!(p.api_key.as_deref(), Some(\"sk-new\"));\n // explicit_cache must survive re-auth\n assert_eq!(p.explicit_cache, Some(true));\n }\n\n #[test]\n fn remove_auth_provider_drops_user_credentials_and_active_selection() {\n let provider = ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n api_key: Some(\"sk-user\".to_string()),\n model: Some(\"user-model\".to_string()),\n ..Default::default()\n };\n let mut config = CoreConfig::default();\n config.ai.active_provider = Some(\"user-provider\".to_string());\n config.ai.active_model = Some(\"user-model\".to_string());\n config\n .ai\n .providers\n .insert(\"user-provider\".to_string(), provider.clone());\n config.user_ai.active_provider = Some(\"user-provider\".to_string());\n config\n .user_ai\n .providers\n .insert(\"user-provider\".to_string(), provider);\n\n let removed = remove_auth_provider(&mut config, \"user-provider\").unwrap();\n\n assert_eq!(removed.active_provider, None);\n assert_eq!(config.ai.active_provider, None);\n assert_eq!(config.ai.active_model, None);\n assert!(!config.ai.providers.contains_key(\"user-provider\"));\n assert!(!config.user_ai.providers.contains_key(\"user-provider\"));\n }\n\n #[test]\n fn remove_auth_provider_reveals_system_provider_with_same_id() {\n let system_provider = ProviderConfig {\n provider_type: Some(\"dashscope\".to_string()),\n api_key: Some(\"sk-system\".to_string()),\n model: Some(\"system-model\".to_string()),\n ..Default::default()\n };\n let user_provider = ProviderConfig {\n provider_type: Some(\"openai\".to_string()),\n api_key: Some(\"sk-user\".to_string()),\n model: Some(\"user-model\".to_string()),\n ..Default::default()\n };\n let mut config = CoreConfig::default();\n config.ai.active_provider = Some(\"shared\".to_string());\n config\n .ai\n .providers\n .insert(\"shared\".to_string(), user_provider.clone());\n config\n .system_ai\n .providers\n .insert(\"shared\".to_string(), system_provider);\n config.system_ai.active_provider = Some(\"shared\".to_string());\n config.user_ai.active_provider = Some(\"shared\".to_string());\n config\n .user_ai\n .providers\n .insert(\"shared\".to_string(), user_provider);\n\n let removed = remove_auth_provider(&mut config, \"shared\").unwrap();\n\n assert_eq!(removed.active_provider.as_deref(), Some(\"shared\"));\n assert_eq!(\n config\n .ai\n .providers\n .get(\"shared\")\n .and_then(|provider| provider.api_key.as_deref()),\n Some(\"sk-system\")\n );\n assert_eq!(config.ai.active_model.as_deref(), Some(\"system-model\"));\n assert_eq!(config.user_ai.active_provider, None);\n }\n\n #[test]\n fn remove_auth_provider_rejects_system_provider() {\n let mut config = CoreConfig::default();\n config\n .ai\n .providers\n .insert(\"system-provider\".to_string(), ProviderConfig::default());\n\n assert_eq!(\n remove_auth_provider(&mut config, \"system-provider\"),\n Err(RemoveAuthProviderError::NotEditable)\n );\n }\n\n #[test]\n fn is_auth_error_detects_401() {\n assert!(is_auth_error(\"API error 401: invalid api key\"));\n assert!(is_auth_error(\"HTTP 403 Forbidden\"));\n assert!(is_auth_error(\"Unauthorized access\"));\n assert!(!is_auth_error(\"API error 500: internal server error\"));\n }\n}\n"}}}}