Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
15 KiB
JSON

{"repo": "aekram43/momo-fetch", "files": {"src/providers.rs": {"da6f823804ef9d857512dbae0370c0e50d6c5ebb": {"t": 1786068334.6603174, "n": 14651, "h": "6b8d22d909e84b39", "c": "use std::collections::HashMap;\nuse std::sync::Arc;\n\nuse adk_model::anthropic::{AnthropicClient, AnthropicConfig};\nuse adk_model::deepseek::DeepSeekClient;\nuse adk_model::groq::{GroqClient, GroqConfig};\nuse adk_model::ollama::{OllamaConfig, OllamaModel};\nuse adk_model::openai::{OpenAIClient, OpenAIConfig};\nuse adk_model::openai_compatible::{OpenAICompatible, OpenAICompatibleConfig};\nuse adk_model::openrouter::{OpenRouterClient, OpenRouterConfig};\nuse adk_rust::prelude::Llm;\n\n/// Configuration for a custom (OpenAI-compatible) provider endpoint.\n#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]\npub struct ProviderConfig {\n pub name: String,\n pub model: String,\n pub api_key_env: String,\n pub base_url: Option<String>,\n pub is_openai_compatible: bool,\n}\n\n/// Manages LLM providers and allows hot-swapping mid-session.\n///\n/// Holds a current `Arc<dyn Llm>` that can be swapped at runtime\n/// via `/model` and `/provider` slash commands.\npub struct ProviderManager {\n current: Arc<dyn Llm>,\n current_provider: String,\n current_model: String,\n custom_endpoints: HashMap<String, ProviderConfig>,\n}\n\nimpl ProviderManager {\n /// Auto-detect provider from environment variables or OS keychain.\n ///\n /// Priority order:\n /// 1. `ANTHROPIC_API_KEY` env or keychain → Anthropic Claude\n /// 2. `OPENAI_API_KEY` env or keychain → OpenAI GPT\n /// 3. `DEEPSEEK_API_KEY` env or keychain → DeepSeek\n /// 4. `GROQ_API_KEY` env or keychain → Groq\n /// 5. `OPENROUTER_API_KEY` env or keychain → OpenRouter\n /// 6. Fallback → Ollama (localhost)\n pub fn from_env() -> anyhow::Result<Self> {\n use crate::config::secrets::SecretStore;\n\n let (provider, model, llm): (String, String, Arc<dyn Llm>) =\n if let Ok(key) = SecretStore::get(\"anthropic\") {\n let model = \"claude-sonnet-4-20250514\".to_string();\n let client = AnthropicClient::new(AnthropicConfig::new(&key, &model))?;\n (\"anthropic\".into(), model, Arc::new(client))\n } else if let Ok(key) = SecretStore::get(\"openai\") {\n let model = \"gpt-4o\".to_string();\n let client = OpenAIClient::new(OpenAIConfig::new(&key, &model))?;\n (\"openai\".into(), model, Arc::new(client))\n } else if let Ok(key) = SecretStore::get(\"deepseek\") {\n let model = \"deepseek-chat\".to_string();\n let client = DeepSeekClient::chat(&key)?;\n (\"deepseek\".into(), model, Arc::new(client))\n } else if let Ok(key) = SecretStore::get(\"groq\") {\n let model = \"llama-3.3-70b-versatile\".to_string();\n let client = GroqClient::new(GroqConfig::new(&key, &model))?;\n (\"groq\".into(), model, Arc::new(client))\n } else if let Ok(key) = SecretStore::get(\"openrouter\") {\n let model = \"anthropic/claude-sonnet-4\".to_string();\n let client = OpenRouterClient::new(OpenRouterConfig::new(&key, &model))?;\n (\"openrouter\".into(), model, Arc::new(client))\n } else if let Ok(key) = SecretStore::get(\"zai\") {\n let model = \"GLM-5\".to_string();\n let base_url = std::env::var(\"ZAI_LLM_URL\")\n .unwrap_or_else(|_| \"https://api.z.ai/api/coding/paas/v4\".to_string());\n let config = OpenAICompatibleConfig::new(&key, &model)\n .with_base_url(&base_url)\n .with_provider_name(\"zai\");\n let client = OpenAICompatible::new(config)?;\n (\"zai\".into(), model, Arc::new(client))\n } else if let (Ok(url), Ok(key)) = (\n std::env::var(\"LLM_URL\"),\n std::env::var(\"LLM_APIKEY\"),\n ) {\n // Generic OpenAI-compatible endpoint via LLM_URL + LLM_APIKEY\n let model = std::env::var(\"LLM_MODEL\")\n .unwrap_or_else(|_| \"default\".to_string());\n let config = OpenAICompatibleConfig::new(&key, &model)\n .with_base_url(&url)\n .with_provider_name(\"custom\");\n let client = OpenAICompatible::new(config)?;\n (\"custom\".into(), model, Arc::new(client))\n } else {\n // Default to ollama (no key needed)\n let model = \"llama3.2\".to_string();\n let client = OllamaModel::new(OllamaConfig::new(&model))?;\n (\"ollama\".into(), model, Arc::new(client))\n };\n\n Ok(Self {\n current: llm,\n current_provider: provider,\n current_model: model,\n custom_endpoints: HashMap::new(),\n })\n }\n\n /// Switch to a different provider and/or model.\n ///\n /// Creates a new LLM instance from environment variables / custom config\n /// and replaces the current model. Returns an error if the provider is\n /// unknown or the required API key is missing.\n pub fn switch(&mut self, provider: &str, model: &str) -> anyhow::Result<()> {\n let new_llm = self.create_model(provider, model)?;\n self.current_provider = provider.to_string();\n self.current_model = model.to_string();\n self.current = new_llm;\n Ok(())\n }\n\n /// Switch only the model, keeping the current provider.\n pub fn switch_model(&mut self, model: &str) -> anyhow::Result<()> {\n let provider = self.current_provider.clone();\n self.switch(&provider, model)\n }\n\n /// Switch only the provider, using its default model.\n pub fn switch_provider(&mut self, provider: &str) -> anyhow::Result<()> {\n let model = default_model_for_provider(provider);\n self.switch(provider, &model)\n }\n\n /// Get the current LLM instance (for passing to agents).\n pub fn current(&self) -> Arc<dyn Llm> {\n self.current.clone()\n }\n\n /// Create a ProviderManager wrapping an existing LLM instance.\n ///\n /// Used by sub-agent spawning to share the same model without\n /// re-reading environment variables.\n pub fn from_current(\n llm: Arc<dyn Llm>,\n provider: String,\n model: String,\n ) -> Self {\n Self {\n current: llm,\n current_provider: provider,\n current_model: model,\n custom_endpoints: HashMap::new(),\n }\n }\n\n /// Get current provider name.\n pub fn current_provider(&self) -> &str {\n &self.current_provider\n }\n\n /// Get current model name.\n pub fn current_model_name(&self) -> &str {\n &self.current_model\n }\n\n /// List available providers based on configured API keys (env or keychain).\n pub fn list_available(&self) -> Vec<ProviderInfo> {\n use crate::config::secrets::SecretStore;\n\n let mut list = Vec::new();\n if SecretStore::get(\"anthropic\").is_ok() {\n list.push(ProviderInfo {\n provider: \"anthropic\".into(),\n default_model: \"claude-sonnet-4-20250514\".into(),\n available: true,\n });\n }\n if SecretStore::get(\"openai\").is_ok() {\n list.push(ProviderInfo {\n provider: \"openai\".into(),\n default_model: \"gpt-4o\".into(),\n available: true,\n });\n }\n if SecretStore::get(\"deepseek\").is_ok() {\n list.push(ProviderInfo {\n provider: \"deepseek\".into(),\n default_model: \"deepseek-chat\".into(),\n available: true,\n });\n }\n if SecretStore::get(\"groq\").is_ok() {\n list.push(ProviderInfo {\n provider: \"groq\".into(),\n default_model: \"llama-3.3-70b-versatile\".into(),\n available: true,\n });\n }\n if SecretStore::get(\"openrouter\").is_ok() {\n list.push(ProviderInfo {\n provider: \"openrouter\".into(),\n default_model: \"anthropic/claude-sonnet-4\".into(),\n available: true,\n });\n }\n if SecretStore::get(\"zai\").is_ok() {\n list.push(ProviderInfo {\n provider: \"zai\".into(),\n default_model: \"GLM-5\".into(),\n available: true,\n });\n }\n // Ollama always available (local)\n list.push(ProviderInfo {\n provider: \"ollama\".into(),\n default_model: \"llama3.2\".into(),\n available: true,\n });\n\n // Add custom endpoints\n for (name, config) in &self.custom_endpoints {\n list.push(ProviderInfo {\n provider: name.clone(),\n default_model: config.model.clone(),\n available: true,\n });\n }\n list\n }\n\n /// Create a model instance for any supported provider.\n fn create_model(&self, provider: &str, model: &str) -> anyhow::Result<Arc<dyn Llm>> {\n use crate::config::secrets::SecretStore;\n\n match provider {\n \"anthropic\" => {\n let key = SecretStore::get(\"anthropic\")\n .map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n let client = AnthropicClient::new(AnthropicConfig::new(&key, model))?;\n Ok(Arc::new(client))\n }\n \"openai\" => {\n let key = SecretStore::get(\"openai\")\n .map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n let client = OpenAIClient::new(OpenAIConfig::new(&key, model))?;\n Ok(Arc::new(client))\n }\n \"deepseek\" => {\n let key = SecretStore::get(\"deepseek\")\n .map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n // Use chat() for deepseek-chat models, reasoner() for deepseek-reasoner\n if model.contains(\"reasoner\") {\n let client = DeepSeekClient::reasoner(&key)?;\n Ok(Arc::new(client))\n } else {\n let client = DeepSeekClient::chat(&key)?;\n Ok(Arc::new(client))\n }\n }\n \"groq\" => {\n let key = SecretStore::get(\"groq\")\n .map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n let client = GroqClient::new(GroqConfig::new(&key, model))?;\n Ok(Arc::new(client))\n }\n \"ollama\" => {\n let client = OllamaModel::new(OllamaConfig::new(model))?;\n Ok(Arc::new(client))\n }\n \"openrouter\" => {\n let key = SecretStore::get(\"openrouter\")\n .map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n let client = OpenRouterClient::new(OpenRouterConfig::new(&key, model))?;\n Ok(Arc::new(client))\n }\n \"zai\" => {\n let key = SecretStore::get(\"zai\")\n .map_err(|e| anyhow::anyhow!(\"{e}\"))?;\n let base_url = std::env::var(\"ZAI_LLM_URL\")\n .unwrap_or_else(|_| \"https://api.z.ai/api/coding/paas/v4\".to_string());\n let config = OpenAICompatibleConfig::new(&key, model)\n .with_base_url(&base_url)\n .with_provider_name(\"zai\");\n let client = OpenAICompatible::new(config)?;\n Ok(Arc::new(client))\n }\n \"custom\" => {\n let url = std::env::var(\"LLM_URL\")\n .map_err(|_| anyhow::anyhow!(\"LLM_URL not set\"))?;\n let key = std::env::var(\"LLM_APIKEY\")\n .map_err(|_| anyhow::anyhow!(\"LLM_APIKEY not set\"))?;\n let config = OpenAICompatibleConfig::new(&key, model)\n .with_base_url(&url)\n .with_provider_name(\"custom\");\n let client = OpenAICompatible::new(config)?;\n Ok(Arc::new(client))\n }\n _ => {\n // Check custom endpoints\n if let Some(config) = self.custom_endpoints.get(provider) {\n let key = std::env::var(&config.api_key_env)\n .map_err(|_| anyhow::anyhow!(\"{} not set\", config.api_key_env))?;\n let mut builder = OpenAICompatibleConfig::new(&key, model);\n if let Some(url) = &config.base_url {\n builder = builder.with_base_url(url);\n }\n let client = OpenAICompatible::new(builder.with_provider_name(provider))?;\n Ok(Arc::new(client))\n } else {\n Err(anyhow::anyhow!(\n \"Unknown provider '{}'. Available: anthropic, openai, deepseek, groq, ollama, openrouter, zai, custom (LLM_URL+LLM_APIKEY)\",\n provider\n ))\n }\n }\n }\n }\n}\n\n/// Info about an available provider.\n#[derive(Debug, Clone)]\npub struct ProviderInfo {\n pub provider: String,\n pub default_model: String,\n pub available: bool,\n}\n\n/// Get the default model name for a given provider.\nfn default_model_for_provider(provider: &str) -> String {\n match provider {\n \"anthropic\" => \"claude-sonnet-4-20250514\".into(),\n \"openai\" => \"gpt-4o\".into(),\n \"deepseek\" => \"deepseek-chat\".into(),\n \"groq\" => \"llama-3.3-70b-versatile\".into(),\n \"ollama\" => \"llama3.2\".into(),\n \"openrouter\" => \"anthropic/claude-sonnet-4\".into(),\n \"zai\" => \"GLM-5\".into(),\n \"custom\" => std::env::var(\"LLM_MODEL\").unwrap_or_else(|_| \"default\".into()),\n _ => \"unknown\".into(),\n }\n}\n\n#[cfg(test)]\nmod tests {\n use super::*;\n\n #[test]\n fn test_default_model_for_provider() {\n assert_eq!(\n default_model_for_provider(\"anthropic\"),\n \"claude-sonnet-4-20250514\"\n );\n assert_eq!(default_model_for_provider(\"openai\"), \"gpt-4o\");\n assert_eq!(default_model_for_provider(\"deepseek\"), \"deepseek-chat\");\n assert_eq!(\n default_model_for_provider(\"groq\"),\n \"llama-3.3-70b-versatile\"\n );\n assert_eq!(default_model_for_provider(\"ollama\"), \"llama3.2\");\n assert_eq!(\n default_model_for_provider(\"openrouter\"),\n \"anthropic/claude-sonnet-4\"\n );\n assert_eq!(default_model_for_provider(\"zai\"), \"GLM-5\");\n // custom provider reads from LLM_MODEL env var, defaults to \"default\"\n assert_eq!(default_model_for_provider(\"custom\"), \"default\");\n assert_eq!(default_model_for_provider(\"unknown\"), \"unknown\");\n }\n}\n"}}}}