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//!
11//! Copyright (c) systemprompt.io — Business Source License 1.1.
12//! See <https://systemprompt.io> for licensing details.
13
14use std::path::Path;
15
16use anyhow::{Context, Result};
17use serde_yaml::Value;
18use systemprompt_identifiers::ProviderId;
19use systemprompt_logging::CliService;
20
21use super::secrets::SecretsData;
22use crate::CliConfig;
23use crate::commands::admin::config::config_section::{read_yaml_file, write_yaml_file};
24
25const STANDARD_PROVIDERS: [&str; 3] = ["gemini", "anthropic", "openai"];
26
27pub(super) fn reconcile(
28    project_root: &Path,
29    primary: &ProviderId,
30    secrets: &SecretsData,
31    config: &CliConfig,
32) -> Result<()> {
33    let path = project_root.join("services").join("ai").join("config.yaml");
34    if !path.exists() {
35        if !config.is_json_output() {
36            CliService::info(&format!(
37                "No {} — skipping AI default-provider reconcile",
38                path.display()
39            ));
40        }
41        return Ok(());
42    }
43
44    let mut doc = read_yaml_file(&path)?;
45    apply_ai_defaults(&mut doc, primary.as_str(), &secrets.present_providers())?;
46    write_yaml_file(&path, &doc)?;
47
48    if !config.is_json_output() {
49        CliService::success(&format!(
50            "Set AI default provider to '{}' in {}",
51            primary.as_str(),
52            path.display()
53        ));
54    }
55    Ok(())
56}
57
58/// Custom providers (e.g. `minimax`) and every field other than the standard
59/// providers' `enabled` flag are left untouched.
60pub fn apply_ai_defaults(doc: &mut Value, default_provider: &str, present: &[&str]) -> Result<()> {
61    let ai = doc
62        .get_mut("ai")
63        .and_then(Value::as_mapping_mut)
64        .context("services/ai/config.yaml has no 'ai' mapping")?;
65
66    ai.insert(
67        Value::String("default_provider".to_owned()),
68        Value::String(default_provider.to_owned()),
69    );
70
71    if let Some(providers) = ai
72        .get_mut(Value::String("providers".to_owned()))
73        .and_then(Value::as_mapping_mut)
74    {
75        for name in STANDARD_PROVIDERS {
76            if let Some(block) = providers
77                .get_mut(Value::String(name.to_owned()))
78                .and_then(Value::as_mapping_mut)
79            {
80                block.insert(
81                    Value::String("enabled".to_owned()),
82                    Value::Bool(present.contains(&name)),
83                );
84            }
85        }
86    }
87
88    Ok(())
89}