Skip to main content

systemprompt_models/services/gateway/config/
runtime.rs

1//! Runtime projection of the gateway configuration.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use std::borrow::Cow;
7use std::collections::HashMap;
8
9use systemprompt_identifiers::{ProviderId, RouteId};
10
11use crate::services::gateway::config::{
12    BridgeReleasesSpec, DEFAULT_ROUTE_PATTERN, GatewayConfigSpec, QuotaFaultMode,
13    default_auth_scheme, default_inference_path_prefix,
14};
15use crate::services::gateway::override_rule::SystemPromptRule;
16use crate::services::gateway::route::GatewayRoute;
17use crate::services::providers::ProviderRegistry;
18use crate::wire::canonical::CanonicalRequest;
19
20/// Runtime gateway configuration: the post-resolution shape every non-loader
21/// caller sees.
22///
23/// Not `Deserialize`: the only legal construction paths are
24/// [`GatewayConfigSpec::resolve`] for the production loader and direct
25/// struct-literal construction in tests.
26#[derive(Debug, Clone)]
27pub struct GatewayConfig {
28    pub enabled: bool,
29    pub routes: Vec<GatewayRoute>,
30    pub default_provider: Option<ProviderId>,
31    pub default_model: Option<String>,
32    pub allow_unlisted_models: bool,
33    pub quota_fault_mode: QuotaFaultMode,
34    pub auth_scheme: String,
35    pub inference_path_prefix: String,
36    pub system_prompt_overrides: Vec<SystemPromptRule>,
37    pub bridge_releases: Option<BridgeReleasesSpec>,
38}
39
40impl Default for GatewayConfig {
41    fn default() -> Self {
42        Self {
43            enabled: false,
44            routes: Vec::new(),
45            default_provider: None,
46            default_model: None,
47            allow_unlisted_models: false,
48            quota_fault_mode: QuotaFaultMode::default(),
49            auth_scheme: default_auth_scheme(),
50            inference_path_prefix: default_inference_path_prefix(),
51            system_prompt_overrides: Vec::new(),
52            bridge_releases: None,
53        }
54    }
55}
56
57impl GatewayConfig {
58    pub fn find_route(&self, model: &str) -> Option<&GatewayRoute> {
59        self.routes.iter().find(|route| route.matches(model))
60    }
61
62    pub fn candidate_routes<'a>(
63        &'a self,
64        registry: &ProviderRegistry,
65    ) -> impl Iterator<Item = Cow<'a, GatewayRoute>> {
66        self.routes
67            .iter()
68            .map(Cow::Borrowed)
69            .chain(self.synthesize_default_route(registry).map(Cow::Owned))
70    }
71
72    #[must_use]
73    pub fn resolve_route<'a>(
74        &'a self,
75        registry: &ProviderRegistry,
76        request: &CanonicalRequest,
77    ) -> Option<Cow<'a, GatewayRoute>> {
78        self.candidate_routes(registry)
79            .find(|route| route.matches_request(request))
80    }
81
82    #[must_use]
83    pub fn dispatchable_route_ids(&self, registry: &ProviderRegistry) -> Vec<RouteId> {
84        let mut ids: Vec<RouteId> = Vec::new();
85        let mut seen: std::collections::HashSet<RouteId> = std::collections::HashSet::new();
86        for route in self.candidate_routes(registry) {
87            let mut route = route.into_owned();
88            route.ensure_id();
89            if seen.insert(route.id.clone()) {
90                ids.push(route.id);
91            }
92        }
93        ids
94    }
95
96    fn synthesize_default_route(&self, registry: &ProviderRegistry) -> Option<GatewayRoute> {
97        let provider = self.default_provider.as_ref()?;
98        registry.find_provider(provider.as_str())?;
99        let mut route = GatewayRoute {
100            id: RouteId::new(""),
101            model_pattern: DEFAULT_ROUTE_PATTERN.to_owned(),
102            provider: provider.clone(),
103            upstream_model: None,
104            extra_headers: HashMap::new(),
105            pricing: None,
106            when: None,
107            requires: None,
108        };
109        route.ensure_id();
110        Some(route)
111    }
112
113    #[must_use]
114    pub fn is_model_exposed(&self, registry: &ProviderRegistry, model: &str) -> bool {
115        if self.find_route(model).is_some() || registry.contains_model(model) {
116            return true;
117        }
118        if self.default_provider.is_some() && self.allow_unlisted_models {
119            tracing::warn!(
120                model,
121                "gateway forwarding an unlisted model to default_provider \
122                 (allow_unlisted_models=true): open allowlist posture"
123            );
124            return true;
125        }
126        false
127    }
128
129    #[must_use]
130    pub fn to_spec(&self) -> GatewayConfigSpec {
131        GatewayConfigSpec {
132            enabled: self.enabled,
133            routes: self.routes.clone(),
134            default_provider: self.default_provider.clone(),
135            default_model: self.default_model.clone(),
136            allow_unlisted_models: self.allow_unlisted_models,
137            quota_fault_mode: self.quota_fault_mode,
138            auth_scheme: self.auth_scheme.clone(),
139            inference_path_prefix: self.inference_path_prefix.clone(),
140            system_prompt_overrides: self.system_prompt_overrides.clone(),
141            bridge_releases: self.bridge_releases.clone(),
142        }
143    }
144}