1use std::collections::HashMap;
12use std::sync::LazyLock;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum ControllerType {
19 Persistent,
21 Subprocess,
24 Acp,
27}
28
29#[derive(Debug)]
34pub struct ProviderConfig {
35 pub id: &'static str,
37 pub display_name: &'static str,
39 pub cli_command: &'static str,
41 pub controller_type: ControllerType,
43 pub launch_args: &'static [&'static str],
46 pub persistent_launch_args: Option<&'static [&'static str]>,
49 pub resume_flag: Option<&'static str>,
52 pub session_id_field: &'static str,
55 pub styled_output_format: &'static str,
57 pub auth_config_dir_env_var: &'static str,
61 pub auth_dir_name: &'static str,
63 pub auth_extra_env: &'static [(&'static str, &'static str)],
66 pub unset_env: &'static [&'static str],
69 pub npm_package: &'static str,
73 pub pinned_version: &'static str,
75 pub icon: &'static str,
78 pub docs_url: &'static str,
80}
81
82impl ProviderConfig {
83 pub fn controller_type_str(&self) -> &'static str {
85 match self.controller_type {
86 ControllerType::Persistent => "persistent",
87 ControllerType::Subprocess => "subprocess",
88 ControllerType::Acp => "acp",
89 }
90 }
91}
92
93static CLAUDE: ProviderConfig = ProviderConfig {
96 id: "claude",
97 display_name: "Claude Code",
98 cli_command: "claude",
99 controller_type: ControllerType::Subprocess,
100 launch_args: &[
101 "-p",
102 "--output-format",
103 "stream-json",
104 "--verbose",
105 "--include-partial-messages",
106 "--dangerously-skip-permissions",
107 ],
108 persistent_launch_args: Some(&[
109 "--input-format",
110 "stream-json",
111 "--output-format",
112 "stream-json",
113 "--verbose",
114 "--include-partial-messages",
115 "--dangerously-skip-permissions",
116 ]),
117 resume_flag: Some("--resume"),
118 session_id_field: "session_id",
119 styled_output_format: "claude-stream-json",
120 auth_config_dir_env_var: "CLAUDE_CONFIG_DIR",
121 auth_dir_name: "claude",
122 auth_extra_env: &[],
123 unset_env: &["CLAUDECODE"],
124 npm_package: "@anthropic-ai/claude-code",
125 pinned_version: "latest",
126 icon: "sparkles",
127 docs_url: "https://docs.anthropic.com/claude-code",
128};
129
130static CODEX: ProviderConfig = ProviderConfig {
131 id: "codex",
132 display_name: "Codex CLI",
133 cli_command: "codex",
134 controller_type: ControllerType::Subprocess,
135 launch_args: &[
137 "exec",
138 "--json",
139 "--dangerously-bypass-approvals-and-sandbox",
140 "-",
141 ],
142 persistent_launch_args: None,
145 resume_flag: None,
146 session_id_field: "thread_id",
147 styled_output_format: "codex-json",
148 auth_config_dir_env_var: "CODEX_HOME",
149 auth_dir_name: "codex",
150 auth_extra_env: &[],
151 unset_env: &[],
152 npm_package: "@openai/codex",
153 pinned_version: "0.116.0",
154 icon: "robot",
155 docs_url: "https://platform.openai.com/docs/codex",
156};
157
158static GEMINI: ProviderConfig = ProviderConfig {
159 id: "gemini",
160 display_name: "Gemini CLI",
161 cli_command: "gemini",
162 controller_type: ControllerType::Subprocess,
163 launch_args: &["--output-format", "stream-json", "--yolo", "-p", ""],
166 persistent_launch_args: None,
167 resume_flag: Some("-r"),
168 session_id_field: "session_id",
169 styled_output_format: "gemini-json",
170 auth_config_dir_env_var: "GEMINI_CLI_HOME",
171 auth_dir_name: "gemini",
172 auth_extra_env: &[("GEMINI_FORCE_FILE_STORAGE", "true")],
173 unset_env: &[],
174 npm_package: "@google/gemini-cli",
175 pinned_version: "0.32.1",
176 icon: "diamond",
177 docs_url: "https://ai.google.dev/gemini-cli",
178};
179
180static KIMI: ProviderConfig = ProviderConfig {
181 id: "kimi",
182 display_name: "Kimi Code CLI",
183 cli_command: "kimi",
184 controller_type: ControllerType::Subprocess,
185 launch_args: &[
186 "--print",
187 "--output-format",
188 "stream-json",
189 "--yolo",
190 "-p",
191 "",
192 ],
193 persistent_launch_args: None,
194 resume_flag: None,
195 session_id_field: "session_id",
196 styled_output_format: "kimi-stream-json",
197 auth_config_dir_env_var: "KIMI_SHARE_DIR",
198 auth_dir_name: "kimi",
199 auth_extra_env: &[],
200 unset_env: &[],
201 npm_package: "",
202 pinned_version: "",
203 icon: "moon",
204 docs_url: "https://moonshotai.github.io/kimi-cli/",
205};
206
207static OPENCLAW: ProviderConfig = ProviderConfig {
208 id: "openclaw",
209 display_name: "OpenClaw",
210 cli_command: "openclaw",
222 controller_type: ControllerType::Acp,
223 launch_args: &["acp"],
224 persistent_launch_args: None,
225 resume_flag: None,
227 session_id_field: "sessionId",
228 styled_output_format: "acp",
229 auth_config_dir_env_var: "OPENCLAW_HOME",
230 auth_dir_name: "openclaw",
231 auth_extra_env: &[],
232 unset_env: &[],
233 npm_package: "openclaw",
234 pinned_version: "latest",
235 icon: "lobster",
236 docs_url: "https://docs.openclaw.ai",
237};
238
239static PI: ProviderConfig = ProviderConfig {
240 id: "pi",
241 display_name: "Pi",
242 cli_command: "pi",
243 controller_type: ControllerType::Acp,
244 launch_args: &["--json"],
245 persistent_launch_args: None,
246 resume_flag: None,
247 session_id_field: "sessionId",
248 styled_output_format: "acp",
249 auth_config_dir_env_var: "PI_HOME",
250 auth_dir_name: "pi",
251 auth_extra_env: &[],
252 unset_env: &[],
253 npm_package: "@mariozechner/pi-coding-agent",
254 pinned_version: "latest",
255 icon: "terminal",
256 docs_url: "https://github.com/badlogic/pi-mono",
257};
258
259static COPILOT: ProviderConfig = ProviderConfig {
264 id: "copilot",
265 display_name: "GitHub Copilot CLI",
266 cli_command: "copilot",
267 controller_type: ControllerType::Acp,
268 launch_args: &["--acp"],
269 persistent_launch_args: None,
270 resume_flag: None,
271 session_id_field: "sessionId",
272 styled_output_format: "acp",
273 auth_config_dir_env_var: "COPILOT_HOME",
274 auth_dir_name: "copilot",
275 auth_extra_env: &[],
276 unset_env: &[],
277 npm_package: "@github/copilot",
278 pinned_version: "latest",
279 icon: "github",
280 docs_url: "https://docs.github.com/copilot/concepts/agents/about-copilot-cli",
281};
282
283static REGISTRY: LazyLock<HashMap<&'static str, &'static ProviderConfig>> = LazyLock::new(|| {
286 let mut m = HashMap::new();
287 m.insert(CLAUDE.id, &CLAUDE);
288 m.insert(CODEX.id, &CODEX);
289 m.insert(GEMINI.id, &GEMINI);
290 m.insert(KIMI.id, &KIMI);
291 m.insert(OPENCLAW.id, &OPENCLAW);
292 m.insert(PI.id, &PI);
293 m.insert(COPILOT.id, &COPILOT);
294 m
295});
296
297static ALIASES: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
299 let mut m = HashMap::new();
300 m.insert("claude-code", "claude");
301 m.insert("claude_code", "claude");
302 m.insert("codex-cli", "codex");
303 m.insert("gemini-cli", "gemini");
304 m.insert("kimi-cli", "kimi");
305 m.insert("kimi_code", "kimi");
306 m.insert("openclaw-cli", "openclaw");
307 m.insert("open-claw", "openclaw");
308 m.insert("copilot-cli", "copilot");
309 m.insert("github-copilot", "copilot");
310 m.insert("copilot_cli", "copilot");
311 m
312});
313
314pub fn resolve_provider_alias(id: &str) -> &'static str {
320 ALIASES.get(id).copied().unwrap_or_else(|| {
321 REGISTRY
325 .get_key_value(id)
326 .map(|(k, _)| *k)
327 .unwrap_or("") })
329}
330
331pub fn get_provider(id: &str) -> Option<&'static ProviderConfig> {
336 if let Some(p) = REGISTRY.get(id) {
338 return Some(p);
339 }
340 let canonical = ALIASES.get(id).copied()?;
342 REGISTRY.get(canonical).copied()
343}
344
345pub fn get_provider_list() -> impl Iterator<Item = &'static ProviderConfig> {
347 static ORDER: &[&str] = &["claude", "codex", "gemini", "kimi", "openclaw", "pi", "copilot"];
349 ORDER.iter().filter_map(|id| REGISTRY.get(*id).copied())
350}
351
352#[cfg(test)]
355mod tests {
356 use super::*;
357
358 #[test]
359 fn canonical_ids_resolve() {
360 assert!(get_provider("claude").is_some());
361 assert!(get_provider("codex").is_some());
362 assert!(get_provider("gemini").is_some());
363 assert!(get_provider("kimi").is_some());
364 assert!(get_provider("openclaw").is_some());
365 }
366
367 #[test]
368 fn aliases_resolve() {
369 assert_eq!(get_provider("claude-code").unwrap().id, "claude");
370 assert_eq!(get_provider("claude_code").unwrap().id, "claude");
371 assert_eq!(get_provider("codex-cli").unwrap().id, "codex");
372 assert_eq!(get_provider("gemini-cli").unwrap().id, "gemini");
373 assert_eq!(get_provider("kimi-cli").unwrap().id, "kimi");
374 assert_eq!(get_provider("openclaw-cli").unwrap().id, "openclaw");
375 }
376
377 #[test]
378 fn unknown_returns_none() {
379 assert!(get_provider("unknown-provider").is_none());
380 }
381
382 #[test]
383 fn provider_list_has_seven_entries() {
384 assert_eq!(get_provider_list().count(), 7);
388 }
389
390 #[test]
391 fn claude_subprocess_with_persistent_args_present() {
392 let p = get_provider("claude").unwrap();
393 assert!(p.persistent_launch_args.is_some());
394 assert_eq!(p.controller_type, ControllerType::Subprocess);
395 }
396
397 #[test]
398 fn codex_resume_flag_is_none() {
399 let p = get_provider("codex").unwrap();
400 assert!(p.resume_flag.is_none());
401 assert_eq!(p.controller_type, ControllerType::Subprocess);
402 }
403
404 #[test]
405 fn gemini_auth_extra_env() {
406 let p = get_provider("gemini").unwrap();
407 assert!(p
408 .auth_extra_env
409 .iter()
410 .any(|(k, v)| *k == "GEMINI_FORCE_FILE_STORAGE" && *v == "true"));
411 }
412
413 #[test]
414 fn kimi_is_subprocess_controller() {
415 let p = get_provider("kimi").unwrap();
416 assert_eq!(p.controller_type, ControllerType::Subprocess);
417 assert_eq!(p.controller_type_str(), "subprocess");
418 assert_eq!(p.styled_output_format, "kimi-stream-json");
419 assert_eq!(p.cli_command, "kimi");
420 assert!(p.npm_package.is_empty());
421 }
422
423 #[test]
424 fn openclaw_is_acp_controller() {
425 let p = get_provider("openclaw").unwrap();
426 assert_eq!(p.controller_type, ControllerType::Acp);
427 assert_eq!(p.controller_type_str(), "acp");
428 assert_eq!(p.styled_output_format, "acp");
429 assert!(p.resume_flag.is_none());
430 }
431
432 #[test]
433 fn pi_is_acp_controller() {
434 let p = get_provider("pi").unwrap();
435 assert_eq!(p.controller_type, ControllerType::Acp);
436 assert_eq!(p.controller_type_str(), "acp");
437 assert_eq!(p.styled_output_format, "acp");
438 assert_eq!(p.cli_command, "pi");
439 assert_eq!(p.npm_package, "@mariozechner/pi-coding-agent");
440 }
441}