systemprompt_models/profile/providers/
mod.rs1mod 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, ModelGovernance, 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 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub governance: Option<ModelGovernance>,
63}
64
65impl ProviderModel {
66 #[must_use]
67 pub fn matches(&self, requested: &str) -> bool {
68 self.id.as_str() == requested || self.aliases.iter().any(|a| a.as_str() == requested)
69 }
70
71 #[must_use]
72 pub fn effective_upstream_model<'a>(&'a self, requested: &'a str) -> &'a str {
73 self.upstream_model.as_deref().unwrap_or(requested)
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
78#[serde(deny_unknown_fields)]
79pub struct ProviderEntry {
80 pub name: ProviderId,
81
82 pub wire: WireProtocol,
83
84 pub surface: ApiSurface,
85
86 pub endpoint: String,
87
88 pub api_key_secret: SecretName,
89
90 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
91 pub extra_headers: HashMap<String, String>,
92
93 #[serde(default, skip_serializing_if = "Vec::is_empty")]
94 pub models: Vec<ProviderModel>,
95
96 #[serde(default)]
97 pub governance: ModelGovernance,
98}
99
100impl ProviderEntry {
101 #[must_use]
102 pub fn find_model(&self, requested: &str) -> Option<&ProviderModel> {
103 self.models.iter().find(|m| m.matches(requested))
104 }
105
106 #[must_use]
107 pub fn effective_governance(&self, requested: &str) -> ModelGovernance {
108 self.find_model(requested)
109 .and_then(|m| m.governance)
110 .unwrap_or(self.governance)
111 }
112}
113
114#[derive(Debug, Clone, Default, Serialize, Deserialize, schemars::JsonSchema)]
115#[serde(transparent)]
116pub struct ProviderRegistry {
117 pub providers: Vec<ProviderEntry>,
118}
119
120impl ProviderRegistry {
121 pub fn default_seed() -> ProviderRegistryResult<Self> {
122 let file: DefaultCatalogFile = serde_yaml::from_str(DEFAULT_CATALOG_YAML)
123 .map_err(|e| ProviderRegistryError::InvalidDefaultCatalog(e.to_string()))?;
124 Ok(Self {
125 providers: file.providers,
126 })
127 }
128
129 #[must_use]
130 pub fn find_provider(&self, name: &str) -> Option<&ProviderEntry> {
131 self.providers.iter().find(|p| p.name.as_str() == name)
132 }
133
134 #[must_use]
135 pub fn contains_model(&self, requested: &str) -> bool {
136 self.providers
137 .iter()
138 .any(|p| p.find_model(requested).is_some())
139 }
140
141 pub fn advertised_providers(&self) -> impl Iterator<Item = &ProviderEntry> {
142 self.providers
143 .iter()
144 .filter(|entry| entry.surface.is_advertised())
145 }
146
147 #[must_use]
148 pub fn advertised_model_ids(&self, surfaces: &[ApiSurface]) -> Vec<String> {
149 self.advertised_providers()
150 .filter(|entry| surfaces.is_empty() || surfaces.contains(&entry.surface))
151 .flat_map(|entry| {
152 entry.models.iter().flat_map(|m| {
153 std::iter::once(m.id.as_str().to_owned())
154 .chain(m.aliases.iter().map(|a| a.as_str().to_owned()))
155 })
156 })
157 .collect()
158 }
159
160 pub fn validate(&self) -> ProviderRegistryResult<()> {
161 let trusted = crate::net::trusted_http_hosts_from_env();
162 let mut seen_providers: HashSet<&str> = HashSet::with_capacity(self.providers.len());
163 let mut seen_models: HashSet<&str> = HashSet::new();
164
165 for provider in &self.providers {
166 if !seen_providers.insert(provider.name.as_str()) {
167 return Err(ProviderRegistryError::DuplicateProvider {
168 name: provider.name.as_str().to_owned(),
169 });
170 }
171 if provider.endpoint.is_empty() {
172 return Err(ProviderRegistryError::EmptyEndpoint {
173 name: provider.name.as_str().to_owned(),
174 });
175 }
176 crate::net::validate_outbound_url_with_trust(&provider.endpoint, &trusted).map_err(
177 |e| ProviderRegistryError::BlockedEndpoint {
178 provider: provider.name.as_str().to_owned(),
179 endpoint: provider.endpoint.clone(),
180 reason: e.to_string(),
181 },
182 )?;
183
184 for model in &provider.models {
185 if model.id.as_str().is_empty() {
186 return Err(ProviderRegistryError::EmptyModelId {
187 id: provider.name.as_str().to_owned(),
188 });
189 }
190 if !seen_models.insert(model.id.as_str()) {
191 return Err(ProviderRegistryError::DuplicateModel {
192 id: model.id.as_str().to_owned(),
193 });
194 }
195 for alias in &model.aliases {
196 if !seen_models.insert(alias.as_str()) {
197 return Err(ProviderRegistryError::DuplicateModel {
198 id: alias.as_str().to_owned(),
199 });
200 }
201 }
202 }
203 }
204 Ok(())
205 }
206}