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

1 line
154 KiB
JSON

{"repo": "0xkurt/clido", "files": {"crates/clido-providers/src/lib.rs": {"cb0bb55beb4fded973b211d88f58fe1d0f26ad43": {"t": 1785746460.6559856, "n": 11136, "h": "9694bfb1009bd3e7", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod backoff;\npub mod fallback;\npub mod http_client;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\npub(crate) mod sse;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{is_subscription_provider, ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| {\n // Provider-specific default user agents\n if provider_name == \"kimi-code\" {\n \"RooCode/3.0.0\".to_string()\n } else {\n format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\"))\n }\n });\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key, model, &ua,\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key, model, url, headers, &ua,\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "5c0736c8f4c485e7805d8640a6968df0f6891f91": {"t": 1785764569.3134525, "n": 11136, "h": "9694bfb1009bd3e7", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod backoff;\npub mod fallback;\npub mod http_client;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\npub(crate) mod sse;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{is_subscription_provider, ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| {\n // Provider-specific default user agents\n if provider_name == \"kimi-code\" {\n \"RooCode/3.0.0\".to_string()\n } else {\n format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\"))\n }\n });\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key, model, &ua,\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key, model, url, headers, &ua,\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "cbb986a7136ca91fb538254153492a3e916faa61": {"t": 1785764570.6095352, "n": 10930, "h": "abdaa8025110a11f", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod backoff;\npub mod fallback;\npub mod http_client;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\npub(crate) mod sse;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{is_subscription_provider, ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key, model, &ua,\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key, model, url, headers, &ua,\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "99b2101263833a55af54d6b624ec25dbbd77bb00": {"t": 1785764572.1863544, "n": 10910, "h": "b31865d0cbf633dc", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod backoff;\npub mod fallback;\npub mod http_client;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{is_subscription_provider, ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key, model, &ua,\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key, model, url, headers, &ua,\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "bec264c33618de32149511fe234f08f727bec193": {"t": 1785764573.4287872, "n": 10983, "h": "74513d1c9125cb34", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod fallback;\npub mod http_client;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{is_subscription_provider, ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key,\n model,\n Some(ua),\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key,\n model,\n url,\n headers,\n Some(ua),\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "ee8f9f7d6e9d369fab4bf9641b0f7715020a2109": {"t": 1785764574.7644746, "n": 10962, "h": "0b7082f5bb04e0ff", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod fallback;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{is_subscription_provider, ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key,\n model,\n Some(ua),\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key,\n model,\n url,\n headers,\n Some(ua),\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "ba93d49fe7c8f0088ac4fc5f7b9b2462cd0ec13a": {"t": 1785764576.7677927, "n": 10936, "h": "8725977a121f5e3e", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod fallback;\npub mod openai;\npub mod provider;\npub mod registry;\npub mod retry;\n\npub use anthropic::AnthropicProvider;\npub use fallback::FallbackProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{ProviderDef, PROVIDER_REGISTRY};\npub use retry::RetryProvider;\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key,\n model,\n Some(ua),\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key,\n model,\n url,\n headers,\n Some(ua),\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "aed69d3c0268750e396f65eb93640ac342acb713": {"t": 1785764579.0993583, "n": 10837, "h": "6fdab94244e2c2a9", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\npub mod registry;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{ProviderDef, PROVIDER_REGISTRY};\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `CLIDO_USER_AGENT` environment variable — process-wide override (e.g. set in test scripts)\n/// 3. `\"clido/<version>\"` — default\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| {\n std::env::var(\"CLIDO_USER_AGENT\")\n .ok()\n .filter(|s| !s.is_empty())\n })\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key,\n model,\n Some(ua),\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key,\n model,\n url,\n headers,\n Some(ua),\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "b8f7f690d154985e53bdea24ca24eb3200d5ed8a": {"t": 1785764580.8692715, "n": 10774, "h": "4a04d9c0d6369ec7", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\npub mod registry;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{ProviderDef, PROVIDER_REGISTRY};\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n///\n/// Resolution order (first non-empty wins):\n/// 1. `user_agent` argument — explicit override from profile/slot config\n/// 2. `ProviderDef::required_user_agent` — provider-specific default (e.g. Kimi Code)\n/// 3. `\"clido/<version>\"` — global fallback\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n let ua = user_agent\n .filter(|s| !s.is_empty())\n .or_else(|| def.required_user_agent.map(|s| s.to_string()))\n .unwrap_or_else(|| format!(\"clido/{}\", env!(\"CARGO_PKG_VERSION\")));\n\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key,\n model,\n Some(ua),\n )));\n }\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key,\n model,\n url,\n headers,\n Some(ua),\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "fb78e61918a831e4038790562239423606ef4f6b": {"t": 1785764583.1920288, "n": 10362, "h": "fdf30de7ad0a68fa", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\npub mod registry;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\npub use registry::{ProviderDef, PROVIDER_REGISTRY};\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n build_provider_with_ua(provider_name, api_key, model, base_url, None)\n}\n\n/// Like [`build_provider`] but allows overriding the HTTP `User-Agent` header.\n/// When `user_agent` is `None`, the default `\"clido/<version>\"` is used.\npub fn build_provider_with_ua(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n user_agent: Option<String>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n if let Some(def) = PROVIDER_REGISTRY.iter().find(|d| d.id == provider_name) {\n if def.is_anthropic {\n return Ok(Arc::new(AnthropicProvider::new_with_user_agent(\n api_key, model, user_agent,\n )));\n }\n // Local and alibabacloud support a runtime base_url override.\n let url = if def.is_local || def.id == \"alibabacloud\" {\n base_url.unwrap_or(def.base_url).to_string()\n } else {\n def.base_url.to_string()\n };\n let headers: Vec<(String, String)> = def\n .extra_headers\n .iter()\n .map(|(k, v)| (k.to_string(), v.to_string()))\n .collect();\n Ok(Arc::new(OpenAICompatProvider::new_with_user_agent(\n api_key, model, url, headers, user_agent,\n )))\n } else {\n let valid: Vec<&str> = PROVIDER_REGISTRY.iter().map(|d| d.id).collect();\n Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Valid: {}.\",\n provider_name,\n valid.join(\", \")\n )))\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "2f90279843b848f0ffb134bc00ae5ddfa62ed681": {"t": 1785764585.5622952, "n": 11034, "h": "c283435222344c04", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Resolve common model aliases to canonical model IDs.\n/// Returns the original string unchanged if no alias matches.\npub fn resolve_model_alias(model: &str) -> &str {\n match model {\n \"sonnet\" => \"claude-sonnet-4-5\",\n \"opus\" => \"claude-opus-4-6\",\n \"haiku\" => \"claude-haiku-4-5\",\n \"4o\" => \"gpt-4o\",\n \"4o-mini\" => \"gpt-4o-mini\",\n \"flash\" => \"gemini-2.5-flash\",\n \"deepseek\" => \"deepseek-chat\",\n \"r1\" => \"deepseek-reasoner\",\n \"grok\" => \"grok-3-beta\",\n \"sonar\" => \"sonar-pro\",\n other => other,\n }\n}\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n let model = resolve_model_alias(&model).to_string();\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"openai\" => Ok(Arc::new(OpenAICompatProvider::new_openai(api_key, model))),\n \"mistral\" => Ok(Arc::new(OpenAICompatProvider::new_mistral(api_key, model))),\n \"minimax\" => Ok(Arc::new(OpenAICompatProvider::new_minimax(api_key, model))),\n \"kimi\" => Ok(Arc::new(OpenAICompatProvider::new_kimi(api_key, model))),\n \"kimi-code\" => Ok(Arc::new(OpenAICompatProvider::new_kimi_code(api_key, model))),\n \"deepseek\" => Ok(Arc::new(OpenAICompatProvider::new_deepseek(api_key, model))),\n \"groq\" => Ok(Arc::new(OpenAICompatProvider::new_groq(api_key, model))),\n \"cerebras\" => Ok(Arc::new(OpenAICompatProvider::new_cerebras(api_key, model))),\n \"togetherai\" => Ok(Arc::new(OpenAICompatProvider::new_togetherai(api_key, model))),\n \"fireworks\" => Ok(Arc::new(OpenAICompatProvider::new_fireworks(api_key, model))),\n \"xai\" => Ok(Arc::new(OpenAICompatProvider::new_xai(api_key, model))),\n \"perplexity\" => Ok(Arc::new(OpenAICompatProvider::new_perplexity(api_key, model))),\n \"gemini\" => Ok(Arc::new(OpenAICompatProvider::new_gemini(api_key, model))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n \"alibabacloud\" => {\n let url = base_url\n .unwrap_or(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n .to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Available: anthropic, openrouter, openai, mistral, minimax, kimi, kimi-code, deepseek, groq, cerebras, togetherai, fireworks, xai, perplexity, gemini, local, alibabacloud.\",\n p\n ))),\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_deepseek_returns_ok() {\n let p = build_provider(\n \"deepseek\",\n \"sk-ds-fake\".to_string(),\n \"deepseek-chat\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_groq_returns_ok() {\n let p = build_provider(\n \"groq\",\n \"gsk_fake\".to_string(),\n \"llama-3.3-70b-versatile\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_cerebras_returns_ok() {\n let p = build_provider(\n \"cerebras\",\n \"csk-fake\".to_string(),\n \"llama-3.3-70b\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_togetherai_returns_ok() {\n let p = build_provider(\n \"togetherai\",\n \"tok-fake\".to_string(),\n \"meta-llama/Llama-3-70b-chat-hf\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_fireworks_returns_ok() {\n let p = build_provider(\n \"fireworks\",\n \"fw-fake\".to_string(),\n \"accounts/fireworks/models/llama-v3p1-70b-instruct\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_xai_returns_ok() {\n let p = build_provider(\n \"xai\",\n \"xai-fake\".to_string(),\n \"grok-3-beta\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_perplexity_returns_ok() {\n let p = build_provider(\n \"perplexity\",\n \"pplx-fake\".to_string(),\n \"sonar-pro\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_gemini_returns_ok() {\n let p = build_provider(\n \"gemini\",\n \"AIzaSy-fake\".to_string(),\n \"gemini-2.5-pro-exp-03-25\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn resolve_model_alias_known() {\n assert_eq!(resolve_model_alias(\"sonnet\"), \"claude-sonnet-4-5\");\n assert_eq!(resolve_model_alias(\"opus\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"haiku\"), \"claude-haiku-4-5\");\n assert_eq!(resolve_model_alias(\"4o\"), \"gpt-4o\");\n assert_eq!(resolve_model_alias(\"4o-mini\"), \"gpt-4o-mini\");\n assert_eq!(resolve_model_alias(\"flash\"), \"gemini-2.5-flash\");\n assert_eq!(resolve_model_alias(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(resolve_model_alias(\"r1\"), \"deepseek-reasoner\");\n assert_eq!(resolve_model_alias(\"grok\"), \"grok-3-beta\");\n assert_eq!(resolve_model_alias(\"sonar\"), \"sonar-pro\");\n }\n\n #[test]\n fn resolve_model_alias_passthrough() {\n assert_eq!(\n resolve_model_alias(\"gpt-4o-2024-11-20\"),\n \"gpt-4o-2024-11-20\"\n );\n assert_eq!(resolve_model_alias(\"claude-opus-4-6\"), \"claude-opus-4-6\");\n assert_eq!(resolve_model_alias(\"\"), \"\");\n }\n\n #[test]\n fn build_provider_alias_is_resolved() {\n // \"sonnet\" alias should produce a working provider (not error)\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"sonnet\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "777de753f73d9e1a34ac6a081ddc9f79e560afb5": {"t": 1785764587.9422398, "n": 5861, "h": "42fa22ce19bc2856", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"openai\" => Ok(Arc::new(OpenAICompatProvider::new_openai(api_key, model))),\n \"mistral\" => Ok(Arc::new(OpenAICompatProvider::new_mistral(api_key, model))),\n \"minimax\" => Ok(Arc::new(OpenAICompatProvider::new_minimax(api_key, model))),\n \"kimi\" => Ok(Arc::new(OpenAICompatProvider::new_kimi(api_key, model))),\n \"kimi-code\" => Ok(Arc::new(OpenAICompatProvider::new_kimi_code(api_key, model))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n \"alibabacloud\" => {\n let url = base_url\n .unwrap_or(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n .to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Available: anthropic, openrouter, openai, mistral, minimax, kimi, kimi-code, local, alibabacloud.\",\n p\n ))),\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_code_returns_ok() {\n let p = build_provider(\n \"kimi-code\",\n \"sk-kimi-fake\".to_string(),\n \"kimi-for-coding\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "9068bb42ed8309a74f9ee6c76fe8220908377adc": {"t": 1785764589.6176543, "n": 5462, "h": "f47bcfe167d50322", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"openai\" => Ok(Arc::new(OpenAICompatProvider::new_openai(api_key, model))),\n \"mistral\" => Ok(Arc::new(OpenAICompatProvider::new_mistral(api_key, model))),\n \"minimax\" => Ok(Arc::new(OpenAICompatProvider::new_minimax(api_key, model))),\n \"kimi\" => Ok(Arc::new(OpenAICompatProvider::new_kimi(api_key, model))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n \"alibabacloud\" => {\n let url = base_url\n .unwrap_or(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n .to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Available: anthropic, openrouter, openai, mistral, minimax, kimi, local, alibabacloud.\",\n p\n ))),\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_kimi_returns_ok() {\n let p = build_provider(\n \"kimi\",\n \"sk-kimi-fake\".to_string(),\n \"moonshot-v1-32k\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "522d5cc4694231eeecbbde0e02bdf7516fca15bf": {"t": 1785764590.863946, "n": 5088, "h": "f1fe323f36d4a424", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"openai\" => Ok(Arc::new(OpenAICompatProvider::new_openai(api_key, model))),\n \"mistral\" => Ok(Arc::new(OpenAICompatProvider::new_mistral(api_key, model))),\n \"minimax\" => Ok(Arc::new(OpenAICompatProvider::new_minimax(api_key, model))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n \"alibabacloud\" => {\n let url = base_url\n .unwrap_or(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n .to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Available: anthropic, openrouter, openai, mistral, minimax, local, alibabacloud.\",\n p\n ))),\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_minimax_returns_ok() {\n let p = build_provider(\n \"minimax\",\n \"sk-minimax-fake\".to_string(),\n \"MiniMax-M2.7\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "28843c60a2530dfea0ca34dfa75e9004f6d438b6": {"t": 1785764592.5668669, "n": 4699, "h": "3a6d7ac96df23f4a", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelEntry, ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"openai\" => Ok(Arc::new(OpenAICompatProvider::new_openai(api_key, model))),\n \"mistral\" => Ok(Arc::new(OpenAICompatProvider::new_mistral(api_key, model))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n \"alibabacloud\" => {\n let url = base_url\n .unwrap_or(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n .to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not supported. Available: anthropic, openrouter, openai, mistral, local, alibabacloud.\",\n p\n ))),\n }\n}\n\n/// Fetch models from a provider's API using the given credentials.\n/// Returns an empty vec on any error (bad key, network failure, unsupported provider).\n/// Used during setup to populate the model selection list dynamically.\n/// Entries with `available = false` have no usable chat endpoints (shown greyed-out).\npub async fn fetch_provider_models(\n provider_name: &str,\n api_key: &str,\n base_url: Option<&str>,\n) -> Vec<ModelEntry> {\n let Ok(provider) = build_provider(\n provider_name,\n api_key.to_string(),\n \"placeholder\".to_string(),\n base_url,\n ) else {\n return vec![];\n };\n provider.list_models().await\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n\n #[test]\n fn build_provider_anthropic_returns_ok() {\n let p = build_provider(\n \"anthropic\",\n \"sk-ant-fake\".to_string(),\n \"claude-sonnet-4-5\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_default_url() {\n let p = build_provider(\"local\", \"\".to_string(), \"llama3.2\".to_string(), None).unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_local_with_custom_url() {\n let p = build_provider(\n \"local\",\n \"\".to_string(),\n \"mistral\".to_string(),\n Some(\"http://127.0.0.1:8080\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_default_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-plus\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_alibabacloud_custom_url() {\n let p = build_provider(\n \"alibabacloud\",\n \"sk-fake\".to_string(),\n \"qwen-turbo\".to_string(),\n Some(\"https://custom.dashscope.com/v1\"),\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n}\n"}, "49838e7c127bd26894de7299b1bb58c5efd8b799": {"t": 1785764593.8873613, "n": 2408, "h": "152cb4db766c912c", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n \"alibabacloud\" => {\n let url = base_url\n .unwrap_or(\"https://dashscope.aliyuncs.com/compatible-mode/v1\")\n .to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not yet supported. Available: anthropic, openrouter, local, alibabacloud.\",\n p\n ))),\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not yet supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n}\n"}, "cc4dba1e6178b9dc49f5f887d76227cfeb457adf": {"t": 1785764595.3183928, "n": 2051, "h": "723ca22258c635b8", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n \"local\" => {\n let url = base_url.unwrap_or(\"http://localhost:11434\").to_string();\n Ok(Arc::new(OpenAICompatProvider::new(\n api_key,\n model,\n url,\n Vec::new(),\n )))\n }\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not yet supported. Available: anthropic, openrouter, local.\",\n p\n ))),\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not yet supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n}\n"}, "2b8564f8f526a4f93ac4cb2cef725a331e5ed979": {"t": 1785764596.701879, "n": 1770, "h": "6c4d371b866a5795", "c": "//! Model providers (Anthropic, OpenRouter, etc.).\n\nuse std::sync::Arc;\n\npub mod anthropic;\npub mod openai;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use openai::OpenAICompatProvider;\npub use provider::{ModelProvider, StreamEvent};\n\nuse clido_core::{ClidoError, Result};\n\n/// Build a provider from profile name, API key, model, and optional base URL.\n/// Used by the CLI after resolving profile and reading API key from env.\npub fn build_provider(\n provider_name: &str,\n api_key: String,\n model: String,\n _base_url: Option<&str>,\n) -> Result<Arc<dyn ModelProvider>> {\n match provider_name {\n \"anthropic\" => Ok(Arc::new(AnthropicProvider::new(api_key, model))),\n \"openrouter\" => Ok(Arc::new(OpenAICompatProvider::new_openrouter(\n api_key, model,\n ))),\n p => Err(ClidoError::Config(format!(\n \"Provider '{}' is not yet supported. Available: anthropic, openrouter.\",\n p\n ))),\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn build_provider_openrouter_returns_ok() {\n let p = build_provider(\n \"openrouter\",\n \"sk-fake\".to_string(),\n \"anthropic/claude-3-haiku\".to_string(),\n None,\n )\n .unwrap();\n assert!(Arc::strong_count(&p) >= 1);\n }\n\n #[test]\n fn build_provider_unsupported_returns_config_error() {\n let res = build_provider(\"unsupported\", \"key\".to_string(), \"model\".to_string(), None);\n let err = match &res {\n Ok(_) => panic!(\"expected Err\"),\n Err(e) => e.to_string(),\n };\n assert!(err.contains(\"not yet supported\"));\n assert!(err.contains(\"anthropic\"));\n assert!(err.contains(\"openrouter\"));\n }\n}\n"}, "3e5aa619670bef6fdc27de2f7efb856554062f31": {"t": 1785764598.2009695, "n": 164, "h": "808edd03809067ee", "c": "//! Model providers (Anthropic, etc.).\n\npub mod anthropic;\npub mod provider;\n\npub use anthropic::AnthropicProvider;\npub use provider::{ModelProvider, StreamEvent};\n"}}}}