Skip to main content

systemprompt_models/services/providers/
mod.rs

1//! Provider registry: the single source of upstream connectivity.
2//!
3//! [`ProviderRegistry`] is the `providers:` list of the services tree
4//! (`services/ai/providers.yaml` by convention, merged across includes). It is
5//! implementation configuration shipped with the deployment, not a
6//! per-environment profile section: the same catalog boots every environment
7//! and only the credentials it names differ. Each [`ProviderEntry`] declares
8//! one upstream exactly once — its
9//! [`WireProtocol`], endpoint, credential ([`SecretName`]), extra headers, and
10//! the model catalog it serves. The two policy layers reference entries by
11//! [`ProviderId`] and never re-declare connectivity: the gateway policy
12//! (`services.gateway`) routes external model names to a provider, and the AI
13//! policy (`services/ai/config.yaml`) selects an agent default and per-provider
14//! overrides.
15//!
16//! Validation here is the authority for connectivity: unique provider names,
17//! SSRF-guarded endpoints, and globally-unique model ids/aliases. The gateway
18//! and AI layers validate only their references *into* this registry.
19//!
20//! Copyright (c) systemprompt.io — Business Source License 1.1.
21//! See <https://systemprompt.io> for licensing details.
22
23mod error;
24mod protocol;
25mod surface;
26
27use std::collections::{HashMap, HashSet};
28
29use serde::{Deserialize, Serialize};
30use systemprompt_identifiers::{ModelId, ProviderId, SecretName};
31
32use crate::services::ai::{ModelCapabilities, ModelGovernance, ModelLimits, ModelPricing};
33
34pub use error::{ProviderRegistryError, ProviderRegistryResult};
35pub use protocol::WireProtocol;
36pub use surface::ApiSurface;
37
38const DEFAULT_CATALOG_YAML: &str = include_str!("default_catalog.yaml");
39
40#[derive(Deserialize)]
41struct DefaultCatalogFile {
42    providers: Vec<ProviderEntry>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
46#[serde(deny_unknown_fields)]
47pub struct ProviderModel {
48    pub id: ModelId,
49
50    #[serde(default, skip_serializing_if = "Vec::is_empty")]
51    pub aliases: Vec<ModelId>,
52
53    #[serde(default, skip_serializing_if = "Option::is_none")]
54    pub upstream_model: Option<String>,
55
56    #[serde(default)]
57    pub pricing: ModelPricing,
58
59    #[serde(default)]
60    pub capabilities: ModelCapabilities,
61
62    #[serde(default)]
63    pub limits: ModelLimits,
64
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub governance: Option<ModelGovernance>,
67}
68
69impl ProviderModel {
70    #[must_use]
71    pub fn matches(&self, requested: &str) -> bool {
72        self.id.as_str() == requested || self.aliases.iter().any(|a| a.as_str() == requested)
73    }
74
75    #[must_use]
76    pub fn effective_upstream_model<'a>(&'a self, requested: &'a str) -> &'a str {
77        self.upstream_model.as_deref().unwrap_or(requested)
78    }
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
82#[serde(deny_unknown_fields)]
83pub struct ProviderEntry {
84    pub name: ProviderId,
85
86    pub wire: WireProtocol,
87
88    pub surface: ApiSurface,
89
90    pub endpoint: String,
91
92    pub api_key_secret: SecretName,
93
94    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
95    pub extra_headers: HashMap<String, String>,
96
97    #[serde(default, skip_serializing_if = "Vec::is_empty")]
98    pub models: Vec<ProviderModel>,
99
100    #[serde(default)]
101    pub governance: ModelGovernance,
102}
103
104impl ProviderEntry {
105    #[must_use]
106    pub fn find_model(&self, requested: &str) -> Option<&ProviderModel> {
107        self.models.iter().find(|m| m.matches(requested))
108    }
109
110    #[must_use]
111    pub fn upstream_model_for<'a>(
112        &'a self,
113        route_override: Option<&'a str>,
114        requested: &'a str,
115    ) -> &'a str {
116        route_override.unwrap_or_else(|| {
117            self.find_model(requested)
118                .map_or(requested, |model| model.effective_upstream_model(requested))
119        })
120    }
121
122    #[must_use]
123    pub fn effective_governance(&self, requested: &str) -> ModelGovernance {
124        self.find_model(requested)
125            .and_then(|m| m.governance)
126            .unwrap_or(self.governance)
127    }
128}
129
130#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
131#[serde(transparent)]
132pub struct ProviderRegistry {
133    pub providers: Vec<ProviderEntry>,
134}
135
136impl ProviderRegistry {
137    pub fn default_seed() -> ProviderRegistryResult<Self> {
138        let file: DefaultCatalogFile = serde_yaml::from_str(DEFAULT_CATALOG_YAML)
139            .map_err(|e| ProviderRegistryError::InvalidDefaultCatalog(e.to_string()))?;
140        Ok(Self {
141            providers: file.providers,
142        })
143    }
144
145    #[must_use]
146    pub fn find_provider(&self, name: &str) -> Option<&ProviderEntry> {
147        self.providers.iter().find(|p| p.name.as_str() == name)
148    }
149
150    #[must_use]
151    pub fn contains_model(&self, requested: &str) -> bool {
152        self.providers
153            .iter()
154            .any(|p| p.find_model(requested).is_some())
155    }
156
157    pub fn advertised_providers(&self) -> impl Iterator<Item = &ProviderEntry> {
158        self.providers
159            .iter()
160            .filter(|entry| entry.surface.is_advertised())
161    }
162
163    #[must_use]
164    pub fn advertised_model_ids(&self, surfaces: &[ApiSurface]) -> Vec<String> {
165        self.advertised_providers()
166            .filter(|entry| surfaces.is_empty() || surfaces.contains(&entry.surface))
167            .flat_map(|entry| {
168                entry.models.iter().flat_map(|m| {
169                    std::iter::once(m.id.as_str().to_owned())
170                        .chain(m.aliases.iter().map(|a| a.as_str().to_owned()))
171                })
172            })
173            .collect()
174    }
175
176    pub fn validate(&self) -> ProviderRegistryResult<()> {
177        let trusted = crate::net::trusted_http_hosts_from_env();
178        let mut seen_providers: HashSet<&str> = HashSet::with_capacity(self.providers.len());
179        let mut seen_models: HashSet<&str> = HashSet::new();
180
181        for provider in &self.providers {
182            if !seen_providers.insert(provider.name.as_str()) {
183                return Err(ProviderRegistryError::DuplicateProvider {
184                    name: provider.name.as_str().to_owned(),
185                });
186            }
187            if provider.endpoint.is_empty() {
188                return Err(ProviderRegistryError::EmptyEndpoint {
189                    name: provider.name.as_str().to_owned(),
190                });
191            }
192            crate::net::validate_outbound_url_with_trust(&provider.endpoint, &trusted).map_err(
193                |e| ProviderRegistryError::BlockedEndpoint {
194                    provider: provider.name.as_str().to_owned(),
195                    endpoint: provider.endpoint.clone(),
196                    reason: e.to_string(),
197                },
198            )?;
199
200            for model in &provider.models {
201                if model.id.as_str().is_empty() {
202                    return Err(ProviderRegistryError::EmptyModelId {
203                        id: provider.name.as_str().to_owned(),
204                    });
205                }
206                if !seen_models.insert(model.id.as_str()) {
207                    return Err(ProviderRegistryError::DuplicateModel {
208                        id: model.id.as_str().to_owned(),
209                    });
210                }
211                for alias in &model.aliases {
212                    if !seen_models.insert(alias.as_str()) {
213                        return Err(ProviderRegistryError::DuplicateModel {
214                            id: alias.as_str().to_owned(),
215                        });
216                    }
217                }
218            }
219        }
220        Ok(())
221    }
222}