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). Operators reshape the
9//! result — adding custom providers like `minimax` — by editing
10//! `profile.providers` directly.
11
12use std::collections::HashMap;
13
14use systemprompt_identifiers::{ProviderId, RouteId};
15use systemprompt_models::profile::{GatewayRoute, ProviderRegistry};
16
17use super::secrets::SecretsData;
18
19struct ProviderDefault {
20    name: &'static str,
21    route_pattern: &'static str,
22    /// Codex sends `openai` aliases (`gpt-5.4-mini`) the real API rejects; the
23    /// `openai` default rewrites them to a concrete model rather than passing
24    /// through.
25    default_upstream: Option<&'static str>,
26    present: fn(&SecretsData) -> bool,
27}
28
29const PROVIDER_DEFAULTS: &[ProviderDefault] = &[
30    ProviderDefault {
31        name: "anthropic",
32        route_pattern: "claude-*",
33        default_upstream: None,
34        present: |s| s.anthropic.is_some(),
35    },
36    ProviderDefault {
37        name: "openai",
38        route_pattern: "gpt-*",
39        default_upstream: Some("gpt-5-mini"),
40        present: |s| s.openai.is_some(),
41    },
42    ProviderDefault {
43        name: "gemini",
44        route_pattern: "gemini-*",
45        default_upstream: None,
46        present: |s| s.gemini.is_some(),
47    },
48];
49
50fn present_defaults(secrets: &SecretsData) -> Vec<&'static ProviderDefault> {
51    PROVIDER_DEFAULTS
52        .iter()
53        .filter(|p| (p.present)(secrets))
54        .collect()
55}
56
57pub fn build_routes(secrets: &SecretsData) -> Vec<GatewayRoute> {
58    present_defaults(secrets)
59        .iter()
60        .map(|d| {
61            let mut route = GatewayRoute {
62                id: RouteId::new(""),
63                model_pattern: d.route_pattern.to_owned(),
64                provider: ProviderId::new(d.name),
65                upstream_model: d.default_upstream.map(str::to_owned),
66                extra_headers: HashMap::new(),
67                pricing: None,
68            };
69            route.ensure_id();
70            route
71        })
72        .collect()
73}
74
75pub fn build_registry(secrets: &SecretsData) -> ProviderRegistry {
76    let seed = match ProviderRegistry::default_seed() {
77        Ok(seed) => seed,
78        Err(e) => {
79            tracing::error!(
80                error = %e,
81                "embedded default provider catalog failed to parse; seeding an empty provider \
82                 registry"
83            );
84            return ProviderRegistry::default();
85        },
86    };
87    ProviderRegistry {
88        providers: present_defaults(secrets)
89            .iter()
90            .filter_map(|d| seed.find_provider(d.name).cloned())
91            .collect(),
92    }
93}