Skip to main content

systemprompt_cli/commands/admin/config/
provider.rs

1//! `admin config provider` command: manage AI providers in the
2//! `ai/config.yaml`.
3//!
4//! [`ProviderCommands`] lists providers, sets the default, and toggles a
5//! provider's enabled flag, editing the AI config YAML in place.
6
7use anyhow::Result;
8use clap::{Args, Subcommand};
9
10use super::types::{
11    ConfigSection, ProviderInfo, ProviderListOutput, ProviderSetOutput, read_yaml_file,
12    write_yaml_file,
13};
14use crate::CliConfig;
15use crate::shared::{CommandResult, render_result};
16
17#[derive(Debug, Subcommand)]
18pub enum ProviderCommands {
19    #[command(about = "List AI providers")]
20    List(ListArgs),
21
22    #[command(about = "Set default provider")]
23    Set(SetArgs),
24
25    #[command(about = "Enable a provider")]
26    Enable(EnableArgs),
27
28    #[command(about = "Disable a provider")]
29    Disable(DisableArgs),
30}
31
32#[derive(Debug, Clone, Copy, Args)]
33pub struct ListArgs;
34
35#[derive(Debug, Clone, Args)]
36pub struct SetArgs {
37    #[arg(value_name = "PROVIDER")]
38    pub provider: String,
39}
40
41#[derive(Debug, Clone, Args)]
42pub struct EnableArgs {
43    #[arg(value_name = "PROVIDER")]
44    pub provider: String,
45}
46
47#[derive(Debug, Clone, Args)]
48pub struct DisableArgs {
49    #[arg(value_name = "PROVIDER")]
50    pub provider: String,
51}
52
53pub fn execute(cmd: ProviderCommands, _config: &CliConfig) -> Result<()> {
54    match cmd {
55        ProviderCommands::List(_args) => {
56            let result = list_providers()?;
57            render_result(
58                &CommandResult::table(serde_json::to_value(result)?).with_title("AI Providers"),
59            );
60        },
61        ProviderCommands::Set(args) => {
62            let result = set_default_provider(&args.provider)?;
63            render_result(
64                &CommandResult::card(serde_json::to_value(result)?).with_title("Provider Updated"),
65            );
66        },
67        ProviderCommands::Enable(args) => {
68            let result = set_provider_enabled(&args.provider, true)?;
69            render_result(
70                &CommandResult::card(serde_json::to_value(result)?).with_title("Provider Enabled"),
71            );
72        },
73        ProviderCommands::Disable(args) => {
74            let result = set_provider_enabled(&args.provider, false)?;
75            render_result(
76                &CommandResult::card(serde_json::to_value(result)?).with_title("Provider Disabled"),
77            );
78        },
79    }
80    Ok(())
81}
82
83fn get_ai_config_path() -> Result<std::path::PathBuf> {
84    ConfigSection::Ai.file_path()
85}
86
87fn list_providers() -> Result<ProviderListOutput> {
88    let file_path = get_ai_config_path()?;
89    let content = read_yaml_file(&file_path)?;
90
91    let ai = content
92        .get("ai")
93        .ok_or_else(|| anyhow::anyhow!("Missing 'ai' section in config"))?;
94
95    let default_provider = ai
96        .get("default_provider")
97        .and_then(|v| v.as_str())
98        .unwrap_or("unknown")
99        .to_owned();
100
101    let providers_section = ai.get("providers");
102
103    let mut providers = Vec::new();
104
105    if let Some(serde_yaml::Value::Mapping(providers_map)) = providers_section {
106        for (name, config) in providers_map {
107            let name_str = name.as_str().unwrap_or("unknown").to_owned();
108
109            let enabled = config
110                .get("enabled")
111                .and_then(serde_yaml::Value::as_bool)
112                .unwrap_or(true);
113
114            let model = config
115                .get("default_model")
116                .and_then(|v| v.as_str())
117                .unwrap_or("unknown")
118                .to_owned();
119
120            let endpoint = config
121                .get("endpoint")
122                .and_then(|v| v.as_str())
123                .map(String::from);
124
125            providers.push(ProviderInfo {
126                name: name_str.clone(),
127                enabled,
128                is_default: name_str == default_provider,
129                model,
130                endpoint,
131            });
132        }
133    }
134
135    Ok(ProviderListOutput {
136        providers,
137        default_provider,
138    })
139}
140
141fn set_default_provider(provider: &str) -> Result<ProviderSetOutput> {
142    let file_path = get_ai_config_path()?;
143    let mut content = read_yaml_file(&file_path)?;
144
145    let providers = content
146        .get("ai")
147        .and_then(|ai| ai.get("providers"))
148        .ok_or_else(|| anyhow::anyhow!("Missing providers section"))?;
149
150    if !providers
151        .as_mapping()
152        .is_some_and(|m| m.contains_key(serde_yaml::Value::String(provider.to_owned())))
153    {
154        let available: Vec<String> = providers.as_mapping().map_or_else(Vec::new, |m| {
155            m.keys()
156                .filter_map(|k| k.as_str().map(String::from))
157                .collect()
158        });
159        anyhow::bail!(
160            "Unknown provider: '{}'. Available providers: {:?}",
161            provider,
162            available
163        );
164    }
165
166    if let Some(serde_yaml::Value::Mapping(ai_map)) = content.get_mut("ai") {
167        ai_map.insert(
168            serde_yaml::Value::String("default_provider".to_owned()),
169            serde_yaml::Value::String(provider.to_owned()),
170        );
171    }
172
173    write_yaml_file(&file_path, &content)?;
174
175    Ok(ProviderSetOutput {
176        provider: provider.to_owned(),
177        action: "set_default".to_owned(),
178        message: format!("Default provider set to '{}'", provider),
179    })
180}
181
182fn set_provider_enabled(provider: &str, enabled: bool) -> Result<ProviderSetOutput> {
183    let file_path = get_ai_config_path()?;
184    let mut content = read_yaml_file(&file_path)?;
185
186    let ai = content
187        .get_mut("ai")
188        .ok_or_else(|| anyhow::anyhow!("Missing 'ai' section"))?;
189
190    let providers = ai
191        .get_mut("providers")
192        .ok_or_else(|| anyhow::anyhow!("Missing 'providers' section"))?;
193
194    let provider_config = providers
195        .get_mut(provider)
196        .ok_or_else(|| anyhow::anyhow!("Unknown provider: '{}'", provider))?;
197
198    if let serde_yaml::Value::Mapping(config_map) = provider_config {
199        config_map.insert(
200            serde_yaml::Value::String("enabled".to_owned()),
201            serde_yaml::Value::Bool(enabled),
202        );
203    }
204
205    write_yaml_file(&file_path, &content)?;
206
207    let action = if enabled { "enabled" } else { "disabled" };
208
209    Ok(ProviderSetOutput {
210        provider: provider.to_owned(),
211        action: action.to_owned(),
212        message: format!("Provider '{}' {}", provider, action),
213    })
214}