systemprompt_models/profile/gateway/
config.rs1use std::borrow::Cow;
9use std::collections::HashMap;
10
11use serde::{Deserialize, Serialize};
12use systemprompt_identifiers::{ProviderId, RouteId};
13
14use super::super::providers::ProviderRegistry;
15use super::error::{GatewayProfileError, GatewayResult};
16use super::override_rule::SystemPromptRule;
17use super::route::GatewayRoute;
18use crate::wire::canonical::CanonicalRequest;
19
20pub(crate) const DEFAULT_ROUTE_PATTERN: &str = "*";
21
22#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
23#[serde(deny_unknown_fields)]
24pub struct GatewayConfigSpec {
25 #[serde(default)]
26 pub enabled: bool,
27 #[serde(default)]
28 pub routes: Vec<GatewayRoute>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub default_provider: Option<ProviderId>,
34 #[serde(default)]
39 pub allow_unlisted_models: bool,
40 #[serde(default = "default_auth_scheme")]
41 pub auth_scheme: String,
42 #[serde(default = "default_inference_path_prefix")]
43 pub inference_path_prefix: String,
44 #[serde(default, skip_serializing_if = "Vec::is_empty")]
45 pub system_prompt_overrides: Vec<SystemPromptRule>,
46}
47
48impl Default for GatewayConfigSpec {
49 fn default() -> Self {
50 Self {
51 enabled: false,
52 routes: Vec::new(),
53 default_provider: None,
54 allow_unlisted_models: false,
55 auth_scheme: default_auth_scheme(),
56 inference_path_prefix: default_inference_path_prefix(),
57 system_prompt_overrides: Vec::new(),
58 }
59 }
60}
61
62fn default_auth_scheme() -> String {
63 "bearer".to_owned()
64}
65
66fn default_inference_path_prefix() -> String {
67 "/v1".to_owned()
68}
69
70impl GatewayConfigSpec {
71 #[must_use]
72 pub fn resolve(self) -> GatewayConfig {
73 let Self {
74 enabled,
75 routes,
76 default_provider,
77 allow_unlisted_models,
78 auth_scheme,
79 inference_path_prefix,
80 system_prompt_overrides,
81 } = self;
82
83 GatewayConfig {
84 enabled,
85 routes,
86 default_provider,
87 allow_unlisted_models,
88 auth_scheme,
89 inference_path_prefix,
90 system_prompt_overrides,
91 }
92 }
93}
94
95#[derive(Debug, Clone)]
102pub struct GatewayConfig {
103 pub enabled: bool,
104 pub routes: Vec<GatewayRoute>,
105 pub default_provider: Option<ProviderId>,
106 pub allow_unlisted_models: bool,
107 pub auth_scheme: String,
108 pub inference_path_prefix: String,
109 pub system_prompt_overrides: Vec<SystemPromptRule>,
110}
111
112impl Default for GatewayConfig {
113 fn default() -> Self {
114 Self {
115 enabled: false,
116 routes: Vec::new(),
117 default_provider: None,
118 allow_unlisted_models: false,
119 auth_scheme: default_auth_scheme(),
120 inference_path_prefix: default_inference_path_prefix(),
121 system_prompt_overrides: Vec::new(),
122 }
123 }
124}
125
126impl GatewayConfig {
127 pub fn find_route(&self, model: &str) -> Option<&GatewayRoute> {
128 self.routes.iter().find(|route| route.matches(model))
129 }
130
131 pub fn candidate_routes<'a>(
132 &'a self,
133 registry: &ProviderRegistry,
134 ) -> impl Iterator<Item = Cow<'a, GatewayRoute>> {
135 self.routes
136 .iter()
137 .map(Cow::Borrowed)
138 .chain(self.synthesize_default_route(registry).map(Cow::Owned))
139 }
140
141 #[must_use]
145 pub fn resolve_route<'a>(
146 &'a self,
147 registry: &ProviderRegistry,
148 request: &CanonicalRequest,
149 ) -> Option<Cow<'a, GatewayRoute>> {
150 self.candidate_routes(registry)
151 .find(|route| route.matches_request(request))
152 }
153
154 #[must_use]
155 pub fn dispatchable_route_ids(&self, registry: &ProviderRegistry) -> Vec<RouteId> {
156 let mut ids: Vec<RouteId> = Vec::new();
157 let mut seen: std::collections::HashSet<RouteId> = std::collections::HashSet::new();
158 for route in self.candidate_routes(registry) {
159 let mut route = route.into_owned();
160 route.ensure_id();
161 if seen.insert(route.id.clone()) {
162 ids.push(route.id);
163 }
164 }
165 ids
166 }
167
168 fn synthesize_default_route(&self, registry: &ProviderRegistry) -> Option<GatewayRoute> {
171 let provider = self.default_provider.as_ref()?;
172 registry.find_provider(provider.as_str())?;
173 let mut route = GatewayRoute {
174 id: RouteId::new(""),
175 model_pattern: DEFAULT_ROUTE_PATTERN.to_owned(),
176 provider: provider.clone(),
177 upstream_model: None,
178 extra_headers: HashMap::new(),
179 pricing: None,
180 when: None,
181 };
182 route.ensure_id();
183 Some(route)
184 }
185
186 #[must_use]
191 pub fn is_model_exposed(&self, registry: &ProviderRegistry, model: &str) -> bool {
192 if self.find_route(model).is_some() || registry.contains_model(model) {
193 return true;
194 }
195 if self.default_provider.is_some() && self.allow_unlisted_models {
196 tracing::warn!(
197 model,
198 "gateway forwarding an unlisted model to default_provider \
199 (allow_unlisted_models=true): open allowlist posture"
200 );
201 return true;
202 }
203 false
204 }
205
206 pub fn validate(&self, registry: &ProviderRegistry) -> GatewayResult<()> {
207 let mut route_ids: std::collections::HashSet<&str> =
208 std::collections::HashSet::with_capacity(self.routes.len());
209 for route in &self.routes {
210 if !route_ids.insert(route.id.as_str()) {
211 return Err(GatewayProfileError::DuplicateRouteId {
212 id: route.id.as_str().to_owned(),
213 });
214 }
215 }
216 if let Some(provider) = self.default_provider.as_ref()
217 && registry.find_provider(provider.as_str()).is_none()
218 {
219 return Err(GatewayProfileError::DefaultProviderNotInRegistry {
220 provider: provider.as_str().to_owned(),
221 });
222 }
223 for route in &self.routes {
224 if registry.find_provider(route.provider.as_str()).is_none() {
225 return Err(GatewayProfileError::RouteProviderNotInRegistry {
226 route: route.model_pattern.clone(),
227 provider: route.provider.as_str().to_owned(),
228 });
229 }
230 if let Some(when) = route.when.as_ref() {
231 when.validate()?;
232 }
233 }
234 for rule in &self.system_prompt_overrides {
235 rule.validate()?;
236 if let Some(provider) = rule.provider.as_ref()
237 && registry.find_provider(provider.as_str()).is_none()
238 {
239 return Err(GatewayProfileError::OverrideProviderNotInRegistry {
240 provider: provider.as_str().to_owned(),
241 });
242 }
243 }
244 Ok(())
245 }
246
247 #[must_use]
248 pub fn to_spec(&self) -> GatewayConfigSpec {
249 GatewayConfigSpec {
250 enabled: self.enabled,
251 routes: self.routes.clone(),
252 default_provider: self.default_provider.clone(),
253 allow_unlisted_models: self.allow_unlisted_models,
254 auth_scheme: self.auth_scheme.clone(),
255 inference_path_prefix: self.inference_path_prefix.clone(),
256 system_prompt_overrides: self.system_prompt_overrides.clone(),
257 }
258 }
259}