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 effective_governance(&self, requested: &str) -> ModelGovernance {
112        self.find_model(requested)
113            .and_then(|m| m.governance)
114            .unwrap_or(self.governance)
115    }
116}
117
118#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
119#[serde(transparent)]
120pub struct ProviderRegistry {
121    pub providers: Vec<ProviderEntry>,
122}
123
124impl ProviderRegistry {
125    pub fn default_seed() -> ProviderRegistryResult<Self> {
126        let file: DefaultCatalogFile = serde_yaml::from_str(DEFAULT_CATALOG_YAML)
127            .map_err(|e| ProviderRegistryError::InvalidDefaultCatalog(e.to_string()))?;
128        Ok(Self {
129            providers: file.providers,
130        })
131    }
132
133    #[must_use]
134    pub fn find_provider(&self, name: &str) -> Option<&ProviderEntry> {
135        self.providers.iter().find(|p| p.name.as_str() == name)
136    }
137
138    #[must_use]
139    pub fn contains_model(&self, requested: &str) -> bool {
140        self.providers
141            .iter()
142            .any(|p| p.find_model(requested).is_some())
143    }
144
145    pub fn advertised_providers(&self) -> impl Iterator<Item = &ProviderEntry> {
146        self.providers
147            .iter()
148            .filter(|entry| entry.surface.is_advertised())
149    }
150
151    #[must_use]
152    pub fn advertised_model_ids(&self, surfaces: &[ApiSurface]) -> Vec<String> {
153        self.advertised_providers()
154            .filter(|entry| surfaces.is_empty() || surfaces.contains(&entry.surface))
155            .flat_map(|entry| {
156                entry.models.iter().flat_map(|m| {
157                    std::iter::once(m.id.as_str().to_owned())
158                        .chain(m.aliases.iter().map(|a| a.as_str().to_owned()))
159                })
160            })
161            .collect()
162    }
163
164    pub fn validate(&self) -> ProviderRegistryResult<()> {
165        let trusted = crate::net::trusted_http_hosts_from_env();
166        let mut seen_providers: HashSet<&str> = HashSet::with_capacity(self.providers.len());
167        let mut seen_models: HashSet<&str> = HashSet::new();
168
169        for provider in &self.providers {
170            if !seen_providers.insert(provider.name.as_str()) {
171                return Err(ProviderRegistryError::DuplicateProvider {
172                    name: provider.name.as_str().to_owned(),
173                });
174            }
175            if provider.endpoint.is_empty() {
176                return Err(ProviderRegistryError::EmptyEndpoint {
177                    name: provider.name.as_str().to_owned(),
178                });
179            }
180            crate::net::validate_outbound_url_with_trust(&provider.endpoint, &trusted).map_err(
181                |e| ProviderRegistryError::BlockedEndpoint {
182                    provider: provider.name.as_str().to_owned(),
183                    endpoint: provider.endpoint.clone(),
184                    reason: e.to_string(),
185                },
186            )?;
187
188            for model in &provider.models {
189                if model.id.as_str().is_empty() {
190                    return Err(ProviderRegistryError::EmptyModelId {
191                        id: provider.name.as_str().to_owned(),
192                    });
193                }
194                if !seen_models.insert(model.id.as_str()) {
195                    return Err(ProviderRegistryError::DuplicateModel {
196                        id: model.id.as_str().to_owned(),
197                    });
198                }
199                for alias in &model.aliases {
200                    if !seen_models.insert(alias.as_str()) {
201                        return Err(ProviderRegistryError::DuplicateModel {
202                            id: alias.as_str().to_owned(),
203                        });
204                    }
205                }
206            }
207        }
208        Ok(())
209    }
210}