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 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}