Skip to main content

systemprompt_cli/commands/admin/setup/
ai_config.rs

1//! Reconcile `services/ai/config.yaml` with the provider chosen during setup.
2//!
3//! `admin setup` generates the profile and secrets, but the AI
4//! service layer reads `services/ai/config.yaml` for its `default_provider` and
5//! per-provider `enabled` flags. When the operator picks a default provider,
6//! align that file: make the choice the default and disable the standard
7//! providers whose keys were not supplied. Custom providers (e.g. `minimax`)
8//! and every other field are left untouched. An absent file is a no-op —
9//! minimal instances need not ship one.
10
11use std::path::Path;
12
13use anyhow::{Context, Result};
14use serde_yaml::Value;
15use systemprompt_identifiers::ProviderId;
16use systemprompt_logging::CliService;
17
18use super::secrets::SecretsData;
19use crate::CliConfig;
20use crate::commands::admin::config::config_section::{read_yaml_file, write_yaml_file};
21
22const STANDARD_PROVIDERS: [&str; 3] = ["gemini", "anthropic", "openai"];
23
24pub(super) fn reconcile(
25    project_root: &Path,
26    primary: &ProviderId,
27    secrets: &SecretsData,
28    config: &CliConfig,
29) -> Result<()> {
30    let path = project_root.join("services").join("ai").join("config.yaml");
31    if !path.exists() {
32        if !config.is_json_output() {
33            CliService::info(&format!(
34                "No {} — skipping AI default-provider reconcile",
35                path.display()
36            ));
37        }
38        return Ok(());
39    }
40
41    let mut doc = read_yaml_file(&path)?;
42    apply_ai_defaults(&mut doc, primary.as_str(), &secrets.present_providers())?;
43    write_yaml_file(&path, &doc)?;
44
45    if !config.is_json_output() {
46        CliService::success(&format!(
47            "Set AI default provider to '{}' in {}",
48            primary.as_str(),
49            path.display()
50        ));
51    }
52    Ok(())
53}
54
55/// Custom providers (e.g. `minimax`) and every field other than the standard
56/// providers' `enabled` flag are left untouched.
57pub fn apply_ai_defaults(doc: &mut Value, default_provider: &str, present: &[&str]) -> Result<()> {
58    let ai = doc
59        .get_mut("ai")
60        .and_then(Value::as_mapping_mut)
61        .context("services/ai/config.yaml has no 'ai' mapping")?;
62
63    ai.insert(
64        Value::String("default_provider".to_owned()),
65        Value::String(default_provider.to_owned()),
66    );
67
68    if let Some(providers) = ai
69        .get_mut(Value::String("providers".to_owned()))
70        .and_then(Value::as_mapping_mut)
71    {
72        for name in STANDARD_PROVIDERS {
73            if let Some(block) = providers
74                .get_mut(Value::String(name.to_owned()))
75                .and_then(Value::as_mapping_mut)
76            {
77                block.insert(
78                    Value::String("enabled".to_owned()),
79                    Value::Bool(present.contains(&name)),
80                );
81            }
82        }
83    }
84
85    Ok(())
86}