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