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, 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}