Skip to main content

systemprompt_models/profile/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::profile::gateway::config::{
12    BridgeReleasesSpec, DEFAULT_ROUTE_PATTERN, GatewayConfigSpec, default_auth_scheme,
13    default_inference_path_prefix,
14};
15use crate::profile::gateway::override_rule::SystemPromptRule;
16use crate::profile::gateway::route::GatewayRoute;
17use crate::profile::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 auth_scheme: String,
34    pub inference_path_prefix: String,
35    pub system_prompt_overrides: Vec<SystemPromptRule>,
36    pub bridge_releases: Option<BridgeReleasesSpec>,
37}
38
39impl Default for GatewayConfig {
40    fn default() -> Self {
41        Self {
42            enabled: false,
43            routes: Vec::new(),
44            default_provider: None,
45            default_model: None,
46            allow_unlisted_models: false,
47            auth_scheme: default_auth_scheme(),
48            inference_path_prefix: default_inference_path_prefix(),
49            system_prompt_overrides: Vec::new(),
50            bridge_releases: None,
51        }
52    }
53}
54
55impl GatewayConfig {
56    pub fn find_route(&self, model: &str) -> Option<&GatewayRoute> {
57        self.routes.iter().find(|route| route.matches(model))
58    }
59
60    pub fn candidate_routes<'a>(
61        &'a self,
62        registry: &ProviderRegistry,
63    ) -> impl Iterator<Item = Cow<'a, GatewayRoute>> {
64        self.routes
65            .iter()
66            .map(Cow::Borrowed)
67            .chain(self.synthesize_default_route(registry).map(Cow::Owned))
68    }
69
70    #[must_use]
71    pub fn resolve_route<'a>(
72        &'a self,
73        registry: &ProviderRegistry,
74        request: &CanonicalRequest,
75    ) -> Option<Cow<'a, GatewayRoute>> {
76        self.candidate_routes(registry)
77            .find(|route| route.matches_request(request))
78    }
79
80    #[must_use]
81    pub fn dispatchable_route_ids(&self, registry: &ProviderRegistry) -> Vec<RouteId> {
82        let mut ids: Vec<RouteId> = Vec::new();
83        let mut seen: std::collections::HashSet<RouteId> = std::collections::HashSet::new();
84        for route in self.candidate_routes(registry) {
85            let mut route = route.into_owned();
86            route.ensure_id();
87            if seen.insert(route.id.clone()) {
88                ids.push(route.id);
89            }
90        }
91        ids
92    }
93
94    fn synthesize_default_route(&self, registry: &ProviderRegistry) -> Option<GatewayRoute> {
95        let provider = self.default_provider.as_ref()?;
96        registry.find_provider(provider.as_str())?;
97        let mut route = GatewayRoute {
98            id: RouteId::new(""),
99            model_pattern: DEFAULT_ROUTE_PATTERN.to_owned(),
100            provider: provider.clone(),
101            upstream_model: None,
102            extra_headers: HashMap::new(),
103            pricing: None,
104            when: None,
105            requires: None,
106        };
107        route.ensure_id();
108        Some(route)
109    }
110
111    #[must_use]
112    pub fn is_model_exposed(&self, registry: &ProviderRegistry, model: &str) -> bool {
113        if self.find_route(model).is_some() || registry.contains_model(model) {
114            return true;
115        }
116        if self.default_provider.is_some() && self.allow_unlisted_models {
117            tracing::warn!(
118                model,
119                "gateway forwarding an unlisted model to default_provider \
120                 (allow_unlisted_models=true): open allowlist posture"
121            );
122            return true;
123        }
124        false
125    }
126
127    #[must_use]
128    pub fn to_spec(&self) -> GatewayConfigSpec {
129        GatewayConfigSpec {
130            enabled: self.enabled,
131            routes: self.routes.clone(),
132            default_provider: self.default_provider.clone(),
133            default_model: self.default_model.clone(),
134            allow_unlisted_models: self.allow_unlisted_models,
135            auth_scheme: self.auth_scheme.clone(),
136            inference_path_prefix: self.inference_path_prefix.clone(),
137            system_prompt_overrides: self.system_prompt_overrides.clone(),
138            bridge_releases: self.bridge_releases.clone(),
139        }
140    }
141}