systemprompt_cli/commands/admin/config/
provider.rs1use 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 );
65 },
66 ProviderCommands::Set(args) => {
67 let result = set_default_provider(&args.provider)?;
68 render_result(&CommandOutput::card_value("Provider Updated", &result));
69 },
70 ProviderCommands::Enable(args) => {
71 let result = set_provider_enabled(&args.provider, true)?;
72 render_result(&CommandOutput::card_value("Provider Enabled", &result));
73 },
74 ProviderCommands::Disable(args) => {
75 let result = set_provider_enabled(&args.provider, false)?;
76 render_result(&CommandOutput::card_value("Provider Disabled", &result));
77 },
78 }
79 Ok(())
80}
81
82fn get_ai_config_path() -> Result<std::path::PathBuf> {
83 ConfigSection::Ai.file_path()
84}
85
86fn list_providers() -> Result<ProviderListOutput> {
87 let registry = &ProfileBootstrap::get()?.providers;
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 = registry
121 .find_provider(&name_str)
122 .map(|entry| entry.endpoint.clone());
123
124 providers.push(ProviderInfo {
125 name: name_str.clone(),
126 enabled,
127 is_default: name_str == default_provider,
128 model,
129 endpoint,
130 });
131 }
132 }
133
134 Ok(ProviderListOutput {
135 providers,
136 default_provider,
137 })
138}
139
140fn set_default_provider(provider: &str) -> Result<ProviderSetOutput> {
141 let registry = &ProfileBootstrap::get()?.providers;
142 if registry.find_provider(provider).is_none() {
143 let available: Vec<&str> = registry.providers.iter().map(|p| p.name.as_str()).collect();
144 anyhow::bail!(
145 "Unknown provider: '{}' is not in profile.providers. Available: {:?}",
146 provider,
147 available
148 );
149 }
150
151 let file_path = get_ai_config_path()?;
152 let mut content = read_yaml_file(&file_path)?;
153
154 let policy = content.get("ai").and_then(|ai| ai.get("providers"));
155 let enabled = policy
156 .and_then(|p| p.get(provider))
157 .and_then(|p| p.get("enabled"))
158 .and_then(serde_yaml::Value::as_bool)
159 .unwrap_or(true);
160 if !enabled {
161 anyhow::bail!(
162 "Provider '{}' is disabled in AI policy; enable it first \
163 (admin config provider enable {})",
164 provider,
165 provider
166 );
167 }
168
169 if let Some(serde_yaml::Value::Mapping(ai_map)) = content.get_mut("ai") {
170 ai_map.insert(
171 serde_yaml::Value::String("default_provider".to_owned()),
172 serde_yaml::Value::String(provider.to_owned()),
173 );
174 }
175
176 write_yaml_file(&file_path, &content)?;
177
178 Ok(ProviderSetOutput {
179 provider: provider.to_owned(),
180 action: "set_default".to_owned(),
181 message: format!("Default provider set to '{}'", provider),
182 })
183}
184
185fn set_provider_enabled(provider: &str, enabled: bool) -> Result<ProviderSetOutput> {
186 let file_path = get_ai_config_path()?;
187 let mut content = read_yaml_file(&file_path)?;
188
189 let ai = content
190 .get_mut("ai")
191 .ok_or_else(|| anyhow::anyhow!("Missing 'ai' section"))?;
192
193 let providers = ai
194 .get_mut("providers")
195 .ok_or_else(|| anyhow::anyhow!("Missing 'providers' section"))?;
196
197 let provider_config = providers
198 .get_mut(provider)
199 .ok_or_else(|| anyhow::anyhow!("Unknown provider: '{}'", provider))?;
200
201 if let serde_yaml::Value::Mapping(config_map) = provider_config {
202 config_map.insert(
203 serde_yaml::Value::String("enabled".to_owned()),
204 serde_yaml::Value::Bool(enabled),
205 );
206 }
207
208 write_yaml_file(&file_path, &content)?;
209
210 let action = if enabled { "enabled" } else { "disabled" };
211
212 Ok(ProviderSetOutput {
213 provider: provider.to_owned(),
214 action: action.to_owned(),
215 message: format!("Provider '{}' {}", provider, action),
216 })
217}