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