systemprompt_models/services/providers/
mod.rs1mod discovery_report;
24mod error;
25mod protocol;
26mod rate_card;
27mod surface;
28
29use std::collections::{HashMap, HashSet};
30
31use serde::{Deserialize, Serialize};
32use systemprompt_identifiers::{ModelId, ProviderId, SecretName};
33
34use crate::services::ai::{ModelCapabilities, ModelGovernance, ModelLimits, ModelPricing};
35
36pub use discovery_report::DiscoveryReport;
37pub use error::{ProviderRegistryError, ProviderRegistryResult};
38pub use protocol::WireProtocol;
39pub use rate_card::{
40 DocumentedLaunchStage, RETIREMENT_NOTICE_DAYS, VertexRateCard, VertexRateCardEntry,
41};
42pub use surface::ApiSurface;
43
44const DEFAULT_CATALOG_YAML: &str = include_str!("default_catalog.yaml");
45
46#[derive(Deserialize)]
47struct DefaultCatalogFile {
48 providers: Vec<ProviderEntry>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
52#[serde(deny_unknown_fields)]
53pub struct ProviderModel {
54 pub id: ModelId,
55
56 #[serde(default, skip_serializing_if = "Vec::is_empty")]
57 pub aliases: Vec<ModelId>,
58
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 pub upstream_model: Option<String>,
61
62 #[serde(default)]
63 pub pricing: ModelPricing,
64
65 #[serde(default)]
66 pub capabilities: ModelCapabilities,
67
68 #[serde(default)]
69 pub limits: ModelLimits,
70
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub governance: Option<ModelGovernance>,
73}
74
75impl ProviderModel {
76 #[must_use]
77 pub fn matches(&self, requested: &str) -> bool {
78 self.id.as_str() == requested || self.aliases.iter().any(|a| a.as_str() == requested)
79 }
80
81 #[must_use]
82 pub fn effective_upstream_model<'a>(&'a self, requested: &'a str) -> &'a str {
83 self.upstream_model.as_deref().unwrap_or(requested)
84 }
85}
86
87#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
88#[serde(deny_unknown_fields)]
89pub struct ProviderEntry {
90 pub name: ProviderId,
91
92 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub display_name: Option<String>,
94
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub description: Option<String>,
97
98 pub wire: WireProtocol,
99
100 pub surface: ApiSurface,
101
102 pub endpoint: String,
103
104 pub api_key_secret: SecretName,
105
106 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
107 pub extra_headers: HashMap<String, String>,
108
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
110 pub models: Vec<ProviderModel>,
111
112 #[serde(default)]
113 pub governance: ModelGovernance,
114}
115
116impl ProviderEntry {
117 #[must_use]
118 pub fn find_model(&self, requested: &str) -> Option<&ProviderModel> {
119 self.models.iter().find(|m| m.matches(requested))
120 }
121
122 #[must_use]
123 pub fn upstream_model_for<'a>(
124 &'a self,
125 route_override: Option<&'a str>,
126 requested: &'a str,
127 ) -> &'a str {
128 let name = route_override.unwrap_or(requested);
129 self.find_model(name)
130 .map_or(name, |model| model.effective_upstream_model(name))
131 }
132
133 #[must_use]
134 pub fn effective_governance(&self, requested: &str) -> ModelGovernance {
135 self.find_model(requested)
136 .and_then(|m| m.governance)
137 .unwrap_or(self.governance)
138 }
139}
140
141#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
142#[serde(transparent)]
143pub struct ProviderRegistry {
144 pub providers: Vec<ProviderEntry>,
145}
146
147impl ProviderRegistry {
148 pub fn default_seed() -> ProviderRegistryResult<Self> {
149 let file: DefaultCatalogFile = serde_yaml::from_str(DEFAULT_CATALOG_YAML)
150 .map_err(|e| ProviderRegistryError::InvalidDefaultCatalog(e.to_string()))?;
151 Ok(Self {
152 providers: file.providers,
153 })
154 }
155
156 #[must_use]
157 pub fn find_provider(&self, name: &str) -> Option<&ProviderEntry> {
158 self.providers.iter().find(|p| p.name.as_str() == name)
159 }
160
161 #[must_use]
162 pub fn contains_model(&self, requested: &str) -> bool {
163 self.providers
164 .iter()
165 .any(|p| p.find_model(requested).is_some())
166 }
167
168 pub fn advertised_providers(&self) -> impl Iterator<Item = &ProviderEntry> {
169 self.providers
170 .iter()
171 .filter(|entry| entry.surface.is_advertised())
172 }
173
174 #[must_use]
175 pub fn advertised_model_ids(&self, surfaces: &[ApiSurface]) -> Vec<String> {
176 self.advertised_providers()
177 .filter(|entry| surfaces.is_empty() || surfaces.contains(&entry.surface))
178 .flat_map(|entry| {
179 entry.models.iter().flat_map(|m| {
180 std::iter::once(m.id.as_str().to_owned())
181 .chain(m.aliases.iter().map(|a| a.as_str().to_owned()))
182 })
183 })
184 .collect()
185 }
186
187 pub fn validate(&self) -> ProviderRegistryResult<()> {
188 let trusted = crate::net::trusted_http_hosts_from_env();
189 let mut seen_providers: HashSet<&str> = HashSet::with_capacity(self.providers.len());
190 let mut seen_models: HashSet<&str> = HashSet::new();
191
192 for provider in &self.providers {
193 if !seen_providers.insert(provider.name.as_str()) {
194 return Err(ProviderRegistryError::DuplicateProvider {
195 name: provider.name.as_str().to_owned(),
196 });
197 }
198 if provider.endpoint.is_empty() {
199 return Err(ProviderRegistryError::EmptyEndpoint {
200 name: provider.name.as_str().to_owned(),
201 });
202 }
203 crate::net::validate_outbound_url_with_trust(&provider.endpoint, &trusted).map_err(
204 |e| ProviderRegistryError::BlockedEndpoint {
205 provider: provider.name.as_str().to_owned(),
206 endpoint: provider.endpoint.clone(),
207 reason: e.to_string(),
208 },
209 )?;
210 if names_a_project_literally(&provider.endpoint) {
211 return Err(ProviderRegistryError::LiteralProjectInEndpoint {
212 provider: provider.name.as_str().to_owned(),
213 endpoint: provider.endpoint.clone(),
214 });
215 }
216
217 for model in &provider.models {
218 if model.id.as_str().is_empty() {
219 return Err(ProviderRegistryError::EmptyModelId {
220 id: provider.name.as_str().to_owned(),
221 });
222 }
223 if !seen_models.insert(model.id.as_str()) {
224 return Err(ProviderRegistryError::DuplicateModel {
225 id: model.id.as_str().to_owned(),
226 });
227 }
228 for alias in &model.aliases {
229 if !seen_models.insert(alias.as_str()) {
230 return Err(ProviderRegistryError::DuplicateModel {
231 id: alias.as_str().to_owned(),
232 });
233 }
234 }
235 }
236 }
237 Ok(())
238 }
239}
240
241pub const PROJECT_PLACEHOLDER: &str = "{project}";
242
243pub const REGION_PLACEHOLDER: &str = "{region}";
250
251#[must_use]
258pub fn names_a_project_literally(endpoint: &str) -> bool {
259 let Ok(url) = url::Url::parse(endpoint) else {
260 return false;
261 };
262 let on_vertex = url.host_str().is_some_and(|host| {
263 host.eq_ignore_ascii_case("aiplatform.googleapis.com")
264 || host
265 .to_ascii_lowercase()
266 .ends_with("-aiplatform.googleapis.com")
267 });
268 if !on_vertex {
269 return false;
270 }
271 let mut segments = url.path_segments().into_iter().flatten();
272 while let Some(segment) = segments.next() {
273 if segment == "projects" {
274 return segments
275 .next()
276 .is_some_and(|id| id != "%7Bproject%7D" && id != PROJECT_PLACEHOLDER);
277 }
278 }
279 false
280}