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