Skip to main content

systemprompt_cli/commands/admin/setup/
services_files.rs

1//! Seeds the provider catalog and gateway routes into the services tree.
2//!
3//! `admin setup` writes `services/ai/providers.yaml` (the embedded default
4//! catalog filtered to the providers whose key was supplied) and
5//! `services/ai/gateway.yaml` (one route per provider plus the default), and
6//! lists both in the root `includes:`. They are written only when absent — a
7//! deployment that already ships its catalog in the image keeps it — unless
8//! `--force` asks for a fresh seed.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use std::path::Path;
14
15use anyhow::{Context, Result};
16use serde::Serialize;
17use systemprompt_identifiers::ProviderId;
18use systemprompt_logging::CliService;
19use systemprompt_models::services::{GatewayConfigSpec, GatewayState, ProviderRegistry};
20
21use super::catalog;
22use super::secrets::SecretsData;
23use crate::commands::admin::config::config_section::{
24    GATEWAY_FILE_RELATIVE, GATEWAY_INCLUDE_RELATIVE, PROVIDERS_FILE_RELATIVE,
25    PROVIDERS_INCLUDE_RELATIVE,
26};
27use crate::commands::admin::config::services_io::append_include;
28
29#[derive(Serialize)]
30struct ProvidersFile {
31    providers: ProviderRegistry,
32}
33
34#[derive(Serialize)]
35struct GatewayFile {
36    gateway: GatewayState,
37}
38
39pub(super) fn seed(
40    services_dir: &Path,
41    secrets: &SecretsData,
42    default_provider: Option<&ProviderId>,
43    force: bool,
44) -> Result<()> {
45    let registry = catalog::build_registry(secrets);
46    let gateway = GatewayState::Spec(GatewayConfigSpec {
47        enabled: true,
48        routes: catalog::build_routes(secrets),
49        default_provider: default_provider.cloned(),
50        ..GatewayConfigSpec::default()
51    });
52
53    registry
54        .validate()
55        .context("generated provider registry failed validation")?;
56    gateway
57        .clone()
58        .into_spec()
59        .resolve()
60        .validate(&registry)
61        .context("generated gateway config failed validation")?;
62
63    let providers_header = "# Provider catalog — models, pricing, capabilities, limits.\n# \
64                            Implementation configuration shipped with the deployment; \
65                            credentials\n# are named by `api_key_secret` and live in the profile \
66                            secret store.\n";
67    let gateway_header = "# Gateway routes — external model patterns onto providers declared \
68                          in\n# providers.yaml. Edit with `systemprompt admin config gateway`.\n";
69
70    write_if_absent(
71        services_dir,
72        PROVIDERS_FILE_RELATIVE,
73        providers_header,
74        &ProvidersFile {
75            providers: registry,
76        },
77        force,
78    )?;
79    write_if_absent(
80        services_dir,
81        GATEWAY_FILE_RELATIVE,
82        gateway_header,
83        &GatewayFile { gateway },
84        force,
85    )?;
86
87    let root = services_dir.join("config").join("config.yaml");
88    if root.exists() {
89        append_include(&root, PROVIDERS_INCLUDE_RELATIVE)?;
90        append_include(&root, GATEWAY_INCLUDE_RELATIVE)?;
91    } else {
92        CliService::warning(&format!(
93            "{} not found; list {PROVIDERS_INCLUDE_RELATIVE} and {GATEWAY_INCLUDE_RELATIVE} in its \
94             includes when you create it",
95            root.display()
96        ));
97    }
98    Ok(())
99}
100
101fn write_if_absent<T: Serialize>(
102    services_dir: &Path,
103    relative: &str,
104    header: &str,
105    body: &T,
106    force: bool,
107) -> Result<()> {
108    let path = services_dir.join(relative);
109    if path.exists() && !force {
110        CliService::info(&format!("Keeping existing {}", path.display()));
111        return Ok(());
112    }
113    if let Some(parent) = path.parent() {
114        std::fs::create_dir_all(parent)
115            .with_context(|| format!("Failed to create {}", parent.display()))?;
116    }
117    let yaml = serde_yaml::to_string(body).context("Failed to serialize services file")?;
118    std::fs::write(&path, format!("{header}{yaml}"))
119        .with_context(|| format!("Failed to write {}", path.display()))?;
120    CliService::success(&format!("Saved {}", path.display()));
121    Ok(())
122}