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