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;
19use systemprompt_manifest::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: None,
64                name: None,
65                description: None,
66                model_pattern: d.route_pattern.to_owned(),
67                provider: ProviderId::new(d.name),
68                upstream_model: d.default_upstream.map(str::to_owned),
69                extra_headers: HashMap::new(),
70                pricing: None,
71                when: None,
72                requires: None,
73                fallbacks: Vec::new(),
74                by_scope: None,
75                strategy: systemprompt_manifest::services::SelectionStrategy::Ordered,
76                weight: None,
77                context_fallbacks: Vec::new(),
78            };
79            route.ensure_id();
80            route
81        })
82        .collect()
83}
84
85pub fn build_registry(secrets: &SecretsData) -> ProviderRegistry {
86    let seed = match ProviderRegistry::default_seed() {
87        Ok(seed) => seed,
88        Err(e) => {
89            tracing::error!(
90                error = %e,
91                "embedded default provider catalog failed to parse; seeding an empty provider \
92                 registry"
93            );
94            return ProviderRegistry::default();
95        },
96    };
97    ProviderRegistry {
98        providers: present_defaults(secrets)
99            .iter()
100            .filter_map(|d| seed.find_provider(d.name).cloned())
101            .collect(),
102    }
103}