1 line
21 KiB
JSON
1 line
21 KiB
JSON
{"repo": "bridgerust/umide", "files": {"crates/umide_agent/src/provider.rs": {"e1644c003fec393de23d793de288b514012e3419": {"t": 1785745835.7036865, "n": 5599, "h": "6282da0b869a6d0a", "c": "//! LLM provider configuration (bring-your-own key, multi-provider).\n\nuse crate::error::AgentError;\n\n/// The supported providers. OpenAI, DeepSeek, and Gemini all use the\n/// OpenAI-compatible backend; only base URL, model, and key differ.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ProviderKind {\n Anthropic,\n OpenAi,\n DeepSeek,\n Gemini,\n}\n\nimpl ProviderKind {\n pub fn label(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"Claude\",\n ProviderKind::OpenAi => \"OpenAI\",\n ProviderKind::DeepSeek => \"DeepSeek\",\n ProviderKind::Gemini => \"Gemini\",\n }\n }\n\n pub fn env_var(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"ANTHROPIC_API_KEY\",\n ProviderKind::OpenAi => \"OPENAI_API_KEY\",\n ProviderKind::DeepSeek => \"DEEPSEEK_API_KEY\",\n ProviderKind::Gemini => \"GEMINI_API_KEY\",\n }\n }\n\n pub fn default_base_url(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"https://api.anthropic.com\",\n ProviderKind::OpenAi => \"https://api.openai.com/v1\",\n ProviderKind::DeepSeek => \"https://api.deepseek.com/v1\",\n // Gemini's OpenAI-compatible surface.\n ProviderKind::Gemini => {\n \"https://generativelanguage.googleapis.com/v1beta/openai\"\n }\n }\n }\n\n /// The pre-first-turn placeholder model. Set to the **Balanced** tier, not\n /// the deep/flagship model — the per-turn Auto router (see `models.rs`)\n /// overrides this each turn so simple requests don't hit the deep model.\n pub fn default_model(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"claude-sonnet-5\",\n ProviderKind::OpenAi => \"gpt-5.5\",\n ProviderKind::DeepSeek => \"deepseek-chat\",\n ProviderKind::Gemini => \"gemini-2.0-flash\",\n }\n }\n\n pub fn all() -> [ProviderKind; 4] {\n [\n ProviderKind::Anthropic,\n ProviderKind::OpenAi,\n ProviderKind::DeepSeek,\n ProviderKind::Gemini,\n ]\n }\n}\n\n#[derive(Debug, Clone)]\npub struct ProviderConfig {\n pub kind: ProviderKind,\n pub api_key: String,\n pub base_url: String,\n pub model: String,\n pub max_tokens: u32,\n /// `output_config.effort` — Anthropic only.\n pub effort: Option<String>,\n /// Request adaptive thinking — Anthropic only.\n pub thinking: bool,\n}\n\nimpl ProviderConfig {\n pub fn new(kind: ProviderKind, api_key: impl Into<String>) -> Self {\n let anthropic = kind == ProviderKind::Anthropic;\n Self {\n kind,\n api_key: api_key.into(),\n base_url: kind.default_base_url().to_string(),\n model: kind.default_model().to_string(),\n // Anthropic streams up to 128k; others are smaller. 16k/8k is ample\n // headroom for a single assistant turn.\n max_tokens: if anthropic { 16_000 } else { 8_192 },\n effort: anthropic.then(|| \"xhigh\".to_string()),\n thinking: anthropic,\n }\n }\n\n /// Resolve a key for `kind`: an explicit settings/keychain key first, then\n /// the provider's environment variable. `MissingApiKey` if neither is set.\n pub fn resolve(\n kind: ProviderKind,\n configured_key: Option<String>,\n ) -> Result<Self, AgentError> {\n if let Some(key) = configured_key.filter(|k| !k.trim().is_empty()) {\n return Ok(Self::new(kind, key));\n }\n let env = std::env::var(kind.env_var())\n .ok()\n .filter(|k| !k.trim().is_empty())\n .ok_or(AgentError::MissingApiKey)?;\n Ok(Self::new(kind, env))\n }\n\n pub fn with_model(mut self, model: impl Into<String>) -> Self {\n self.model = model.into();\n self\n }\n\n pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {\n self.base_url = base_url.into();\n self\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn anthropic_defaults() {\n let p = ProviderConfig::new(ProviderKind::Anthropic, \"k\");\n assert_eq!(p.model, \"claude-sonnet-5\");\n assert_eq!(p.base_url, \"https://api.anthropic.com\");\n assert_eq!(p.effort.as_deref(), Some(\"xhigh\"));\n assert!(p.thinking);\n }\n\n #[test]\n fn openai_family_defaults() {\n let o = ProviderConfig::new(ProviderKind::OpenAi, \"k\");\n assert_eq!(o.model, \"gpt-5.5\");\n assert!(o.base_url.contains(\"openai.com\"));\n assert!(o.effort.is_none());\n assert!(!o.thinking);\n\n let d = ProviderConfig::new(ProviderKind::DeepSeek, \"k\");\n assert_eq!(d.model, \"deepseek-chat\");\n assert!(d.base_url.contains(\"deepseek.com\"));\n\n let g = ProviderConfig::new(ProviderKind::Gemini, \"k\");\n assert_eq!(g.model, \"gemini-2.0-flash\");\n assert!(g.base_url.contains(\"generativelanguage\"));\n }\n\n #[test]\n fn resolve_prefers_configured_key() {\n let p =\n ProviderConfig::resolve(ProviderKind::OpenAi, Some(\"explicit\".into()))\n .unwrap();\n assert_eq!(p.api_key, \"explicit\");\n assert_eq!(p.kind, ProviderKind::OpenAi);\n }\n\n #[test]\n fn resolve_rejects_blank_key_when_no_env() {\n if std::env::var(\"DEEPSEEK_API_KEY\").is_err() {\n assert!(matches!(\n ProviderConfig::resolve(ProviderKind::DeepSeek, Some(\" \".into())),\n Err(AgentError::MissingApiKey)\n ));\n }\n }\n}\n"}, "df6a29547791ea6ecc01012c0ee1fddc247c3515": {"t": 1785757047.2521696, "n": 5599, "h": "6282da0b869a6d0a", "c": "//! LLM provider configuration (bring-your-own key, multi-provider).\n\nuse crate::error::AgentError;\n\n/// The supported providers. OpenAI, DeepSeek, and Gemini all use the\n/// OpenAI-compatible backend; only base URL, model, and key differ.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ProviderKind {\n Anthropic,\n OpenAi,\n DeepSeek,\n Gemini,\n}\n\nimpl ProviderKind {\n pub fn label(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"Claude\",\n ProviderKind::OpenAi => \"OpenAI\",\n ProviderKind::DeepSeek => \"DeepSeek\",\n ProviderKind::Gemini => \"Gemini\",\n }\n }\n\n pub fn env_var(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"ANTHROPIC_API_KEY\",\n ProviderKind::OpenAi => \"OPENAI_API_KEY\",\n ProviderKind::DeepSeek => \"DEEPSEEK_API_KEY\",\n ProviderKind::Gemini => \"GEMINI_API_KEY\",\n }\n }\n\n pub fn default_base_url(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"https://api.anthropic.com\",\n ProviderKind::OpenAi => \"https://api.openai.com/v1\",\n ProviderKind::DeepSeek => \"https://api.deepseek.com/v1\",\n // Gemini's OpenAI-compatible surface.\n ProviderKind::Gemini => {\n \"https://generativelanguage.googleapis.com/v1beta/openai\"\n }\n }\n }\n\n /// The pre-first-turn placeholder model. Set to the **Balanced** tier, not\n /// the deep/flagship model — the per-turn Auto router (see `models.rs`)\n /// overrides this each turn so simple requests don't hit the deep model.\n pub fn default_model(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"claude-sonnet-5\",\n ProviderKind::OpenAi => \"gpt-5.5\",\n ProviderKind::DeepSeek => \"deepseek-chat\",\n ProviderKind::Gemini => \"gemini-2.0-flash\",\n }\n }\n\n pub fn all() -> [ProviderKind; 4] {\n [\n ProviderKind::Anthropic,\n ProviderKind::OpenAi,\n ProviderKind::DeepSeek,\n ProviderKind::Gemini,\n ]\n }\n}\n\n#[derive(Debug, Clone)]\npub struct ProviderConfig {\n pub kind: ProviderKind,\n pub api_key: String,\n pub base_url: String,\n pub model: String,\n pub max_tokens: u32,\n /// `output_config.effort` — Anthropic only.\n pub effort: Option<String>,\n /// Request adaptive thinking — Anthropic only.\n pub thinking: bool,\n}\n\nimpl ProviderConfig {\n pub fn new(kind: ProviderKind, api_key: impl Into<String>) -> Self {\n let anthropic = kind == ProviderKind::Anthropic;\n Self {\n kind,\n api_key: api_key.into(),\n base_url: kind.default_base_url().to_string(),\n model: kind.default_model().to_string(),\n // Anthropic streams up to 128k; others are smaller. 16k/8k is ample\n // headroom for a single assistant turn.\n max_tokens: if anthropic { 16_000 } else { 8_192 },\n effort: anthropic.then(|| \"xhigh\".to_string()),\n thinking: anthropic,\n }\n }\n\n /// Resolve a key for `kind`: an explicit settings/keychain key first, then\n /// the provider's environment variable. `MissingApiKey` if neither is set.\n pub fn resolve(\n kind: ProviderKind,\n configured_key: Option<String>,\n ) -> Result<Self, AgentError> {\n if let Some(key) = configured_key.filter(|k| !k.trim().is_empty()) {\n return Ok(Self::new(kind, key));\n }\n let env = std::env::var(kind.env_var())\n .ok()\n .filter(|k| !k.trim().is_empty())\n .ok_or(AgentError::MissingApiKey)?;\n Ok(Self::new(kind, env))\n }\n\n pub fn with_model(mut self, model: impl Into<String>) -> Self {\n self.model = model.into();\n self\n }\n\n pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {\n self.base_url = base_url.into();\n self\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn anthropic_defaults() {\n let p = ProviderConfig::new(ProviderKind::Anthropic, \"k\");\n assert_eq!(p.model, \"claude-sonnet-5\");\n assert_eq!(p.base_url, \"https://api.anthropic.com\");\n assert_eq!(p.effort.as_deref(), Some(\"xhigh\"));\n assert!(p.thinking);\n }\n\n #[test]\n fn openai_family_defaults() {\n let o = ProviderConfig::new(ProviderKind::OpenAi, \"k\");\n assert_eq!(o.model, \"gpt-5.5\");\n assert!(o.base_url.contains(\"openai.com\"));\n assert!(o.effort.is_none());\n assert!(!o.thinking);\n\n let d = ProviderConfig::new(ProviderKind::DeepSeek, \"k\");\n assert_eq!(d.model, \"deepseek-chat\");\n assert!(d.base_url.contains(\"deepseek.com\"));\n\n let g = ProviderConfig::new(ProviderKind::Gemini, \"k\");\n assert_eq!(g.model, \"gemini-2.0-flash\");\n assert!(g.base_url.contains(\"generativelanguage\"));\n }\n\n #[test]\n fn resolve_prefers_configured_key() {\n let p =\n ProviderConfig::resolve(ProviderKind::OpenAi, Some(\"explicit\".into()))\n .unwrap();\n assert_eq!(p.api_key, \"explicit\");\n assert_eq!(p.kind, ProviderKind::OpenAi);\n }\n\n #[test]\n fn resolve_rejects_blank_key_when_no_env() {\n if std::env::var(\"DEEPSEEK_API_KEY\").is_err() {\n assert!(matches!(\n ProviderConfig::resolve(ProviderKind::DeepSeek, Some(\" \".into())),\n Err(AgentError::MissingApiKey)\n ));\n }\n }\n}\n"}, "bdc8557b532c0c26bd0b64bfaf7c3e79d547e3ec": {"t": 1785757048.7483325, "n": 5362, "h": "eec5cea5ce0e21b9", "c": "//! LLM provider configuration (bring-your-own key, multi-provider).\n\nuse crate::error::AgentError;\n\n/// The supported providers. OpenAI, DeepSeek, and Gemini all use the\n/// OpenAI-compatible backend; only base URL, model, and key differ.\n#[derive(Debug, Clone, Copy, PartialEq, Eq)]\npub enum ProviderKind {\n Anthropic,\n OpenAi,\n DeepSeek,\n Gemini,\n}\n\nimpl ProviderKind {\n pub fn label(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"Claude\",\n ProviderKind::OpenAi => \"OpenAI\",\n ProviderKind::DeepSeek => \"DeepSeek\",\n ProviderKind::Gemini => \"Gemini\",\n }\n }\n\n pub fn env_var(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"ANTHROPIC_API_KEY\",\n ProviderKind::OpenAi => \"OPENAI_API_KEY\",\n ProviderKind::DeepSeek => \"DEEPSEEK_API_KEY\",\n ProviderKind::Gemini => \"GEMINI_API_KEY\",\n }\n }\n\n pub fn default_base_url(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"https://api.anthropic.com\",\n ProviderKind::OpenAi => \"https://api.openai.com/v1\",\n ProviderKind::DeepSeek => \"https://api.deepseek.com/v1\",\n // Gemini's OpenAI-compatible surface.\n ProviderKind::Gemini => {\n \"https://generativelanguage.googleapis.com/v1beta/openai\"\n }\n }\n }\n\n pub fn default_model(self) -> &'static str {\n match self {\n ProviderKind::Anthropic => \"claude-opus-4-8\",\n ProviderKind::OpenAi => \"gpt-4o\",\n ProviderKind::DeepSeek => \"deepseek-chat\",\n ProviderKind::Gemini => \"gemini-2.0-flash\",\n }\n }\n\n pub fn all() -> [ProviderKind; 4] {\n [\n ProviderKind::Anthropic,\n ProviderKind::OpenAi,\n ProviderKind::DeepSeek,\n ProviderKind::Gemini,\n ]\n }\n}\n\n#[derive(Debug, Clone)]\npub struct ProviderConfig {\n pub kind: ProviderKind,\n pub api_key: String,\n pub base_url: String,\n pub model: String,\n pub max_tokens: u32,\n /// `output_config.effort` — Anthropic only.\n pub effort: Option<String>,\n /// Request adaptive thinking — Anthropic only.\n pub thinking: bool,\n}\n\nimpl ProviderConfig {\n pub fn new(kind: ProviderKind, api_key: impl Into<String>) -> Self {\n let anthropic = kind == ProviderKind::Anthropic;\n Self {\n kind,\n api_key: api_key.into(),\n base_url: kind.default_base_url().to_string(),\n model: kind.default_model().to_string(),\n // Anthropic streams up to 128k; others are smaller. 16k/8k is ample\n // headroom for a single assistant turn.\n max_tokens: if anthropic { 16_000 } else { 8_192 },\n effort: anthropic.then(|| \"xhigh\".to_string()),\n thinking: anthropic,\n }\n }\n\n /// Resolve a key for `kind`: an explicit settings/keychain key first, then\n /// the provider's environment variable. `MissingApiKey` if neither is set.\n pub fn resolve(\n kind: ProviderKind,\n configured_key: Option<String>,\n ) -> Result<Self, AgentError> {\n if let Some(key) = configured_key.filter(|k| !k.trim().is_empty()) {\n return Ok(Self::new(kind, key));\n }\n let env = std::env::var(kind.env_var())\n .ok()\n .filter(|k| !k.trim().is_empty())\n .ok_or(AgentError::MissingApiKey)?;\n Ok(Self::new(kind, env))\n }\n\n pub fn with_model(mut self, model: impl Into<String>) -> Self {\n self.model = model.into();\n self\n }\n\n pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {\n self.base_url = base_url.into();\n self\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn anthropic_defaults() {\n let p = ProviderConfig::new(ProviderKind::Anthropic, \"k\");\n assert_eq!(p.model, \"claude-opus-4-8\");\n assert_eq!(p.base_url, \"https://api.anthropic.com\");\n assert_eq!(p.effort.as_deref(), Some(\"xhigh\"));\n assert!(p.thinking);\n }\n\n #[test]\n fn openai_family_defaults() {\n let o = ProviderConfig::new(ProviderKind::OpenAi, \"k\");\n assert_eq!(o.model, \"gpt-4o\");\n assert!(o.base_url.contains(\"openai.com\"));\n assert!(o.effort.is_none());\n assert!(!o.thinking);\n\n let d = ProviderConfig::new(ProviderKind::DeepSeek, \"k\");\n assert_eq!(d.model, \"deepseek-chat\");\n assert!(d.base_url.contains(\"deepseek.com\"));\n\n let g = ProviderConfig::new(ProviderKind::Gemini, \"k\");\n assert_eq!(g.model, \"gemini-2.0-flash\");\n assert!(g.base_url.contains(\"generativelanguage\"));\n }\n\n #[test]\n fn resolve_prefers_configured_key() {\n let p =\n ProviderConfig::resolve(ProviderKind::OpenAi, Some(\"explicit\".into()))\n .unwrap();\n assert_eq!(p.api_key, \"explicit\");\n assert_eq!(p.kind, ProviderKind::OpenAi);\n }\n\n #[test]\n fn resolve_rejects_blank_key_when_no_env() {\n if std::env::var(\"DEEPSEEK_API_KEY\").is_err() {\n assert!(matches!(\n ProviderConfig::resolve(ProviderKind::DeepSeek, Some(\" \".into())),\n Err(AgentError::MissingApiKey)\n ));\n }\n }\n}\n"}, "2c33ad83558f2fb1d861734a5dc479973f42d593": {"t": 1785757050.6349535, "n": 3499, "h": "39be4ec519ea39d9", "c": "//! LLM provider configuration.\n//!\n//! Deliberately abstract: today it resolves a BYO (bring-your-own) Anthropic key\n//! and points at the public API, but a future hosted \"UMIDE Pro\" tier or a\n//! self-hosted gateway is just a different `base_url`/key here — the agent loop,\n//! tools, and UI never change.\n\nuse crate::error::AgentError;\nuse crate::types::{DEFAULT_MODEL, FAST_MODEL};\n\n#[derive(Debug, Clone)]\npub struct ProviderConfig {\n pub api_key: String,\n pub base_url: String,\n pub model: String,\n /// Cheaper model for quick auxiliary calls (vision triage, classification).\n pub fast_model: String,\n pub max_tokens: u32,\n /// `output_config.effort`: \"low\"..\"xhigh\"..\"max\". `xhigh` suits coding/agentic.\n pub effort: Option<String>,\n /// Whether to request adaptive thinking on the main loop.\n pub thinking: bool,\n}\n\nimpl ProviderConfig {\n /// Resolve a key from `ANTHROPIC_API_KEY`. Callers that have a key from\n /// settings/keychain should use [`ProviderConfig::with_key`] instead.\n pub fn from_env() -> Result<Self, AgentError> {\n let api_key = std::env::var(\"ANTHROPIC_API_KEY\")\n .ok()\n .filter(|k| !k.trim().is_empty())\n .ok_or(AgentError::MissingApiKey)?;\n Ok(Self::with_key(api_key))\n }\n\n /// Resolution order the editor should use: explicit settings/keychain key,\n /// then `ANTHROPIC_API_KEY`. Returns `MissingApiKey` if neither is present\n /// so the panel can show the \"add your key\" empty state.\n pub fn resolve(configured_key: Option<String>) -> Result<Self, AgentError> {\n if let Some(key) = configured_key.filter(|k| !k.trim().is_empty()) {\n return Ok(Self::with_key(key));\n }\n Self::from_env()\n }\n\n pub fn with_key(api_key: impl Into<String>) -> Self {\n Self {\n api_key: api_key.into(),\n base_url: \"https://api.anthropic.com\".to_string(),\n model: DEFAULT_MODEL.to_string(),\n fast_model: FAST_MODEL.to_string(),\n // Opus 4.8 supports 128k output with streaming; 64k is ample headroom.\n max_tokens: 64_000,\n effort: Some(\"xhigh\".to_string()),\n thinking: true,\n }\n }\n\n pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {\n self.base_url = base_url.into();\n self\n }\n\n pub fn with_model(mut self, model: impl Into<String>) -> Self {\n self.model = model.into();\n self\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn with_key_uses_expected_defaults() {\n let p = ProviderConfig::with_key(\"k\");\n assert_eq!(p.model, \"claude-opus-4-8\");\n assert_eq!(p.fast_model, \"claude-haiku-4-5\");\n assert_eq!(p.effort.as_deref(), Some(\"xhigh\"));\n assert_eq!(p.max_tokens, 64_000);\n assert!(p.thinking);\n assert_eq!(p.base_url, \"https://api.anthropic.com\");\n }\n\n #[test]\n fn resolve_prefers_configured_key() {\n let p = ProviderConfig::resolve(Some(\"explicit\".into())).unwrap();\n assert_eq!(p.api_key, \"explicit\");\n }\n\n #[test]\n fn resolve_rejects_blank_configured_key_when_no_env() {\n // Only assert the blank path when the env var isn't providing a fallback.\n if std::env::var(\"ANTHROPIC_API_KEY\").is_err() {\n assert!(matches!(\n ProviderConfig::resolve(Some(\" \".into())),\n Err(AgentError::MissingApiKey)\n ));\n }\n }\n}\n"}}}} |