Skip to main content

systemprompt_models/profile/providers/
mod.rs

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