Skip to main content

systemprompt_cli/commands/admin/setup/
catalog.rs

1//! Default provider-registry and route generation for the setup wizard.
2//!
3//! The model catalog (providers, models, pricing, capabilities) comes from the
4//! embedded canonical seed [`ProviderRegistry::default_seed`]; this module only
5//! selects the providers whose AI key was actually supplied and emits a
6//! matching [`GatewayRoute`] per provider, so the generated profile resolves
7//! and passes both [`ProviderRegistry::validate`] and `GatewayConfig::validate`
8//! (every route provider must exist in the registry). The result is written to
9//! `services/ai/providers.yaml` and `services/ai/gateway.yaml`; operators
10//! reshape it — adding custom providers like `minimax` — by editing those
11//! files or through `admin config catalog` / `admin config gateway`.
12//!
13//! Copyright (c) systemprompt.io — Business Source License 1.1.
14//! See <https://systemprompt.io> for licensing details.
15
16use std::collections::HashMap;
17
18use systemprompt_identifiers::{ProviderId, RouteId};
19use systemprompt_models::services::{GatewayRoute, ProviderRegistry};
20
21use super::secrets::SecretsData;
22
23struct ProviderDefault {
24    name: &'static str,
25    route_pattern: &'static str,
26    default_upstream: Option<&'static str>,
27    present: fn(&SecretsData) -> bool,
28}
29
30const PROVIDER_DEFAULTS: &[ProviderDefault] = &[
31    ProviderDefault {
32        name: "anthropic",
33        route_pattern: "claude-*",
34        default_upstream: None,
35        present: |s| s.anthropic.is_some(),
36    },
37    ProviderDefault {
38        name: "openai",
39        route_pattern: "gpt-*",
40        default_upstream: Some("gpt-5-mini"),
41        present: |s| s.openai.is_some(),
42    },
43    ProviderDefault {
44        name: "gemini",
45        route_pattern: "gemini-*",
46        default_upstream: None,
47        present: |s| s.gemini.is_some(),
48    },
49];
50
51fn present_defaults(secrets: &SecretsData) -> Vec<&'static ProviderDefault> {
52    PROVIDER_DEFAULTS
53        .iter()
54        .filter(|p| (p.present)(secrets))
55        .collect()
56}
57
58pub fn build_routes(secrets: &SecretsData) -> Vec<GatewayRoute> {
59    present_defaults(secrets)
60        .iter()
61        .map(|d| {
62            let mut route = GatewayRoute {
63                id: RouteId::new(""),
64                model_pattern: d.route_pattern.to_owned(),
65                provider: ProviderId::new(d.name),
66                upstream_model: d.default_upstream.map(str::to_owned),
67                extra_headers: HashMap::new(),
68                pricing: None,
69                when: None,
70                requires: None,
71            };
72            route.ensure_id();
73            route
74        })
75        .collect()
76}
77
78pub fn build_registry(secrets: &SecretsData) -> ProviderRegistry {
79    let seed = match ProviderRegistry::default_seed() {
80        Ok(seed) => seed,
81        Err(e) => {
82            tracing::error!(
83                error = %e,
84                "embedded default provider catalog failed to parse; seeding an empty provider \
85                 registry"
86            );
87            return ProviderRegistry::default();
88        },
89    };
90    ProviderRegistry {
91        providers: present_defaults(secrets)
92            .iter()
93            .filter_map(|d| seed.find_provider(d.name).cloned())
94            .collect(),
95    }
96}