1#![allow(dead_code)]
6
7use serde::{Deserialize, Serialize};
8use std::path::PathBuf;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "kebab-case")]
13pub enum ProviderType {
14 Copilot,
19 Cursor,
21 #[serde(rename = "continuedev")]
23 ContinueDev,
24 #[serde(rename = "codexcli")]
26 CodexCli,
27 #[serde(rename = "droidcli")]
29 DroidCli,
30 #[serde(rename = "geminicli")]
32 GeminiCli,
33 #[serde(rename = "claudecode")]
35 ClaudeCode,
36 #[serde(rename = "opencode")]
38 OpenCode,
39 #[serde(rename = "openclaw")]
41 OpenClaw,
42 #[serde(rename = "antigravity")]
44 Antigravity,
45
46 Ollama,
51 Vllm,
53 Foundry,
55 LmStudio,
57 #[serde(rename = "localai")]
59 LocalAI,
60 #[serde(rename = "text-gen-webui")]
62 TextGenWebUI,
63 Jan,
65 #[serde(rename = "gpt4all")]
67 Gpt4All,
68 Llamafile,
70
71 #[serde(rename = "m365copilot")]
76 M365Copilot,
77 #[serde(rename = "chatgpt")]
79 ChatGPT,
80 #[serde(rename = "openai")]
82 OpenAI,
83 #[serde(rename = "anthropic")]
85 Anthropic,
86 #[serde(rename = "perplexity")]
88 Perplexity,
89 #[serde(rename = "deepseek")]
91 DeepSeek,
92 #[serde(rename = "qwen")]
94 Qwen,
95 #[serde(rename = "gemini")]
97 Gemini,
98 #[serde(rename = "mistral")]
100 Mistral,
101 #[serde(rename = "cohere")]
103 Cohere,
104 #[serde(rename = "grok")]
106 Grok,
107 #[serde(rename = "groq")]
109 Groq,
110 #[serde(rename = "together")]
112 Together,
113 #[serde(rename = "fireworks")]
115 Fireworks,
116 #[serde(rename = "replicate")]
118 Replicate,
119 #[serde(rename = "huggingface")]
121 HuggingFace,
122
123 Custom,
125}
126
127impl ProviderType {
128 pub fn display_name(&self) -> &'static str {
130 match self {
131 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 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 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 pub fn default_endpoint(&self) -> Option<&'static str> {
175 match self {
176 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 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 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 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 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 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 | Self::Groq | Self::Together | Self::Fireworks | Self::Custom
277 )
278 }
279
280 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#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct ProviderConfig {
295 pub provider_type: ProviderType,
297
298 #[serde(default = "default_true")]
300 pub enabled: bool,
301
302 pub endpoint: Option<String>,
304
305 pub api_key: Option<String>,
307
308 pub model: Option<String>,
310
311 pub name: Option<String>,
313
314 pub storage_path: Option<PathBuf>,
316
317 #[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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct CsmConfig {
352 #[serde(default)]
354 pub providers: Vec<ProviderConfig>,
355
356 pub default_provider: Option<ProviderType>,
358
359 #[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, }
371 }
372}
373
374impl CsmConfig {
375 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 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 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 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 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}