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