systemprompt_models/services/providers/
mod.rs1mod 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}