Skip to main content

chasm/providers/
config.rs

1// Copyright (c) 2024-2026 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! Provider configuration and types
4
5#![allow(dead_code)]
6
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10/// Supported LLM provider types
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum ProviderType {
14    // ========================================================================
15    // Local/File-based Providers
16    // ========================================================================
17    /// VS Code GitHub Copilot Chat (default)
18    Copilot,
19    /// Cursor IDE chat
20    Cursor,
21    /// Continue.dev VS Code extension
22    #[serde(rename = "continuedev")]
23    ContinueDev,
24    /// Codex CLI (OpenAI) - JSONL sessions in ~/.codex/sessions/
25    #[serde(rename = "codexcli")]
26    CodexCli,
27    /// Droid CLI (Factory) - JSONL sessions in ~/.factory/sessions/
28    #[serde(rename = "droidcli")]
29    DroidCli,
30    /// Gemini CLI (Google) - JSON sessions in ~/.gemini/tmp/
31    #[serde(rename = "geminicli")]
32    GeminiCli,
33    /// Claude Code (Anthropic) - sessions in ~/.claude/projects/
34    #[serde(rename = "claudecode")]
35    ClaudeCode,
36    /// OpenCode - sessions in ~/.opencode/conversations/
37    #[serde(rename = "opencode")]
38    OpenCode,
39    /// OpenClaw - sessions in ~/.openclaw/chat-history/
40    #[serde(rename = "openclaw")]
41    OpenClaw,
42    /// Antigravity - sessions in workspaceStorage
43    #[serde(rename = "antigravity")]
44    Antigravity,
45
46    // ========================================================================
47    // Local API Providers
48    // ========================================================================
49    /// Ollama local models
50    Ollama,
51    /// vLLM server
52    Vllm,
53    /// Azure AI Foundry / Foundry Local
54    Foundry,
55    /// LM Studio
56    LmStudio,
57    /// LocalAI
58    #[serde(rename = "localai")]
59    LocalAI,
60    /// Text Generation WebUI (oobabooga)
61    #[serde(rename = "text-gen-webui")]
62    TextGenWebUI,
63    /// Jan.ai
64    Jan,
65    /// GPT4All
66    #[serde(rename = "gpt4all")]
67    Gpt4All,
68    /// Llamafile
69    Llamafile,
70
71    // ========================================================================
72    // Cloud API Providers (with conversation history APIs)
73    // ========================================================================
74    /// Microsoft 365 Copilot (enterprise)
75    #[serde(rename = "m365copilot")]
76    M365Copilot,
77    /// OpenAI ChatGPT (cloud)
78    #[serde(rename = "chatgpt")]
79    ChatGPT,
80    /// OpenAI API (for local/custom deployments)
81    #[serde(rename = "openai")]
82    OpenAI,
83    /// Anthropic Claude
84    #[serde(rename = "anthropic")]
85    Anthropic,
86    /// Perplexity AI
87    #[serde(rename = "perplexity")]
88    Perplexity,
89    /// DeepSeek
90    #[serde(rename = "deepseek")]
91    DeepSeek,
92    /// Qwen (Alibaba Cloud)
93    #[serde(rename = "qwen")]
94    Qwen,
95    /// Google Gemini
96    #[serde(rename = "gemini")]
97    Gemini,
98    /// Mistral AI
99    #[serde(rename = "mistral")]
100    Mistral,
101    /// Cohere
102    #[serde(rename = "cohere")]
103    Cohere,
104    /// xAI Grok
105    #[serde(rename = "grok")]
106    Grok,
107    /// Groq
108    #[serde(rename = "groq")]
109    Groq,
110    /// Together AI
111    #[serde(rename = "together")]
112    Together,
113    /// Fireworks AI
114    #[serde(rename = "fireworks")]
115    Fireworks,
116    /// Replicate
117    #[serde(rename = "replicate")]
118    Replicate,
119    /// HuggingFace Inference API
120    #[serde(rename = "huggingface")]
121    HuggingFace,
122
123    /// Custom OpenAI-compatible endpoint
124    Custom,
125}
126
127impl ProviderType {
128    /// Get the display name for this provider
129    pub fn display_name(&self) -> &'static str {
130        match self {
131            // Local/File-based
132            Self::Copilot => "GitHub Copilot",
133            Self::Cursor => "Cursor",
134            Self::ContinueDev => "Continue.dev",
135            Self::CodexCli => "Codex CLI",
136            Self::DroidCli => "Droid CLI",
137            Self::GeminiCli => "Gemini CLI",
138            Self::ClaudeCode => "Claude Code",
139            Self::OpenCode => "OpenCode",
140            Self::OpenClaw => "OpenClaw",
141            Self::Antigravity => "Antigravity",
142            // Local API
143            Self::Ollama => "Ollama",
144            Self::Vllm => "vLLM",
145            Self::Foundry => "Azure AI Foundry",
146            Self::LmStudio => "LM Studio",
147            Self::LocalAI => "LocalAI",
148            Self::TextGenWebUI => "Text Generation WebUI",
149            Self::Jan => "Jan.ai",
150            Self::Gpt4All => "GPT4All",
151            Self::Llamafile => "Llamafile",
152            // Cloud API
153            Self::M365Copilot => "Microsoft 365 Copilot",
154            Self::ChatGPT => "ChatGPT",
155            Self::OpenAI => "OpenAI API",
156            Self::Anthropic => "Anthropic Claude",
157            Self::Perplexity => "Perplexity AI",
158            Self::DeepSeek => "DeepSeek",
159            Self::Qwen => "Qwen (Alibaba)",
160            Self::Gemini => "Google Gemini",
161            Self::Mistral => "Mistral AI",
162            Self::Cohere => "Cohere",
163            Self::Grok => "xAI Grok",
164            Self::Groq => "Groq",
165            Self::Together => "Together AI",
166            Self::Fireworks => "Fireworks AI",
167            Self::Replicate => "Replicate",
168            Self::HuggingFace => "HuggingFace",
169            Self::Custom => "Custom",
170        }
171    }
172
173    /// Get the default API endpoint for this provider
174    pub fn default_endpoint(&self) -> Option<&'static str> {
175        match self {
176            // Local/File-based (no API endpoint)
177            Self::Copilot => None,
178            Self::Cursor => None,
179            Self::ContinueDev => None,
180            Self::CodexCli => None,
181            Self::DroidCli => None,
182            Self::GeminiCli => None,
183            Self::ClaudeCode => None,
184            Self::OpenCode => None,
185            Self::OpenClaw => None,
186            Self::Antigravity => None,
187            // Local API
188            Self::Ollama => Some("http://localhost:11434"),
189            Self::Vllm => Some("http://localhost:8000"),
190            Self::Foundry => Some("http://localhost:5272"),
191            Self::LmStudio => Some("http://localhost:1234/v1"),
192            Self::LocalAI => Some("http://localhost:8080/v1"),
193            Self::TextGenWebUI => Some("http://localhost:5000/v1"),
194            Self::Jan => Some("http://localhost:1337/v1"),
195            Self::Gpt4All => Some("http://localhost:4891/v1"),
196            Self::Llamafile => Some("http://localhost:8080/v1"),
197            // Cloud API
198            Self::M365Copilot => Some("https://graph.microsoft.com/v1.0"),
199            Self::ChatGPT => Some("https://chat.openai.com"),
200            Self::OpenAI => Some("https://api.openai.com/v1"),
201            Self::Anthropic => Some("https://api.anthropic.com/v1"),
202            Self::Perplexity => Some("https://api.perplexity.ai"),
203            Self::DeepSeek => Some("https://api.deepseek.com/v1"),
204            Self::Qwen => Some("https://dashscope.aliyuncs.com/api/v1"),
205            Self::Gemini => Some("https://generativelanguage.googleapis.com/v1beta"),
206            Self::Mistral => Some("https://api.mistral.ai/v1"),
207            Self::Cohere => Some("https://api.cohere.ai/v1"),
208            Self::Grok => Some("https://api.x.ai/v1"),
209            Self::Groq => Some("https://api.groq.com/openai/v1"),
210            Self::Together => Some("https://api.together.xyz/v1"),
211            Self::Fireworks => Some("https://api.fireworks.ai/inference/v1"),
212            Self::Replicate => Some("https://api.replicate.com/v1"),
213            Self::HuggingFace => Some("https://api-inference.huggingface.co"),
214            Self::Custom => None,
215        }
216    }
217
218    /// Check if this provider uses local file storage for sessions
219    pub fn uses_file_storage(&self) -> bool {
220        matches!(
221            self,
222            Self::Copilot
223                | Self::Cursor
224                | Self::ContinueDev
225                | Self::CodexCli
226                | Self::DroidCli
227                | Self::GeminiCli
228                | Self::ClaudeCode
229                | Self::OpenCode
230                | Self::OpenClaw
231                | Self::Antigravity
232        )
233    }
234
235    /// Check if this provider is a cloud-based service with conversation history API
236    pub fn is_cloud_provider(&self) -> bool {
237        matches!(
238            self,
239            Self::M365Copilot
240                | Self::ChatGPT
241                | Self::OpenAI
242                | Self::Anthropic
243                | Self::Perplexity
244                | Self::DeepSeek
245                | Self::Qwen
246                | Self::Gemini
247                | Self::Mistral
248                | Self::Cohere
249                | Self::Grok
250                | Self::Groq
251                | Self::Together
252                | Self::Fireworks
253                | Self::Replicate
254                | Self::HuggingFace
255        )
256    }
257
258    /// Check if this provider supports the OpenAI API format
259    pub fn is_openai_compatible(&self) -> bool {
260        matches!(
261            self,
262            Self::Ollama
263                | Self::Vllm
264                | Self::Foundry
265                | Self::OpenAI
266                | Self::LmStudio
267                | Self::LocalAI
268                | Self::TextGenWebUI
269                | Self::Jan
270                | Self::Gpt4All
271                | Self::Llamafile
272                | Self::DeepSeek  // DeepSeek uses OpenAI-compatible API
273                | Self::Groq      // Groq uses OpenAI-compatible API
274                | Self::Together  // Together uses OpenAI-compatible API
275                | Self::Fireworks // Fireworks uses OpenAI-compatible API
276                | Self::Custom
277        )
278    }
279
280    /// Check if this provider requires an API key
281    pub fn requires_api_key(&self) -> bool {
282        self.is_cloud_provider()
283    }
284}
285
286impl std::fmt::Display for ProviderType {
287    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288        write!(f, "{}", self.display_name())
289    }
290}
291
292/// Configuration for a single provider
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct ProviderConfig {
295    /// Provider type
296    pub provider_type: ProviderType,
297
298    /// Whether this provider is enabled
299    #[serde(default = "default_true")]
300    pub enabled: bool,
301
302    /// API endpoint URL (for server-based providers)
303    pub endpoint: Option<String>,
304
305    /// API key (if required)
306    pub api_key: Option<String>,
307
308    /// Model to use (if configurable)
309    pub model: Option<String>,
310
311    /// Custom name for this provider instance
312    pub name: Option<String>,
313
314    /// Path to session storage (for file-based providers)
315    pub storage_path: Option<PathBuf>,
316
317    /// Additional provider-specific settings
318    #[serde(default)]
319    pub extra: std::collections::HashMap<String, serde_json::Value>,
320}
321
322fn default_true() -> bool {
323    true
324}
325
326impl ProviderConfig {
327    /// Create a new provider config with default settings
328    pub fn new(provider_type: ProviderType) -> Self {
329        Self {
330            provider_type,
331            enabled: true,
332            endpoint: provider_type.default_endpoint().map(String::from),
333            api_key: None,
334            model: None,
335            name: None,
336            storage_path: None,
337            extra: std::collections::HashMap::new(),
338        }
339    }
340
341    /// Get the display name for this provider
342    pub fn display_name(&self) -> String {
343        self.name
344            .clone()
345            .unwrap_or_else(|| self.provider_type.display_name().to_string())
346    }
347}
348
349/// Global CSM configuration including all providers
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct CsmConfig {
352    /// Configured providers
353    #[serde(default)]
354    pub providers: Vec<ProviderConfig>,
355
356    /// Default provider for new sessions
357    pub default_provider: Option<ProviderType>,
358
359    /// Whether to auto-discover providers
360    #[serde(default = "default_true")]
361    pub auto_discover: bool,
362}
363
364impl Default for CsmConfig {
365    fn default() -> Self {
366        Self {
367            providers: Vec::new(),
368            default_provider: None,
369            auto_discover: true, // Important: enable auto-discovery by default
370        }
371    }
372}
373
374impl CsmConfig {
375    /// Load configuration from the default location
376    pub fn load() -> anyhow::Result<Self> {
377        let config_path = Self::config_path()?;
378
379        if config_path.exists() {
380            let content = std::fs::read_to_string(&config_path)?;
381            let config: Self = serde_json::from_str(&content)?;
382            Ok(config)
383        } else {
384            Ok(Self::default())
385        }
386    }
387
388    /// Save configuration to the default location
389    pub fn save(&self) -> anyhow::Result<()> {
390        let config_path = Self::config_path()?;
391
392        if let Some(parent) = config_path.parent() {
393            std::fs::create_dir_all(parent)?;
394        }
395
396        let content = serde_json::to_string_pretty(self)?;
397        std::fs::write(&config_path, content)?;
398        Ok(())
399    }
400
401    /// Get the configuration file path
402    pub fn config_path() -> anyhow::Result<PathBuf> {
403        let config_dir =
404            dirs::config_dir().ok_or_else(|| anyhow::anyhow!("Could not find config directory"))?;
405        Ok(config_dir.join("csm").join("config.json"))
406    }
407
408    /// Get a provider config by type
409    pub fn get_provider(&self, provider_type: ProviderType) -> Option<&ProviderConfig> {
410        self.providers
411            .iter()
412            .find(|p| p.provider_type == provider_type)
413    }
414
415    /// Add or update a provider config
416    pub fn set_provider(&mut self, config: ProviderConfig) {
417        if let Some(existing) = self
418            .providers
419            .iter_mut()
420            .find(|p| p.provider_type == config.provider_type)
421        {
422            *existing = config;
423        } else {
424            self.providers.push(config);
425        }
426    }
427}