Skip to main content

systemprompt_models/services/gateway/config/
mod.rs

1//! Gateway configuration: on-disk spec and resolved runtime form.
2//!
3//! [`GatewayConfigSpec`] is the serde shape accepted under `gateway:` in the
4//! services tree; [`GatewayConfig`] is its runtime projection. Routes carry no
5//! embedded provider catalog — every route resolves its provider against
6//! `services.providers` (the merged `providers:` list of the services tree)
7//! (`ProviderRegistry`) at use time.
8//!
9//! Copyright (c) systemprompt.io — Business Source License 1.1.
10//! See <https://systemprompt.io> for licensing details.
11
12mod runtime;
13mod validate;
14
15use serde::{Deserialize, Serialize};
16use systemprompt_identifiers::ProviderId;
17
18use crate::services::gateway::override_rule::SystemPromptRule;
19use crate::services::gateway::route::GatewayRoute;
20
21pub use runtime::GatewayConfig;
22
23pub(crate) const DEFAULT_ROUTE_PATTERN: &str = "*";
24
25/// What the gateway does when it cannot evaluate a quota or policy.
26///
27/// The switch lives in file config rather than in `GatewayPolicySpec` because
28/// one of the faults it governs is the failure to read that policy row.
29#[derive(
30    Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema,
31)]
32#[serde(rename_all = "lowercase")]
33pub enum QuotaFaultMode {
34    #[default]
35    Open,
36    Closed,
37}
38
39impl QuotaFaultMode {
40    #[must_use]
41    pub const fn is_closed(self) -> bool {
42        matches!(self, Self::Closed)
43    }
44
45    #[must_use]
46    pub const fn as_str(self) -> &'static str {
47        match self {
48            Self::Open => "open",
49            Self::Closed => "closed",
50        }
51    }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
55#[serde(deny_unknown_fields)]
56pub struct GatewayConfigSpec {
57    #[serde(default)]
58    pub enabled: bool,
59    #[serde(default)]
60    pub routes: Vec<GatewayRoute>,
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub default_provider: Option<ProviderId>,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub default_model: Option<String>,
65    #[serde(default)]
66    pub allow_unlisted_models: bool,
67    #[serde(default)]
68    pub quota_fault_mode: QuotaFaultMode,
69    #[serde(default = "default_auth_scheme")]
70    pub auth_scheme: String,
71    #[serde(default = "default_inference_path_prefix")]
72    pub inference_path_prefix: String,
73    #[serde(default, skip_serializing_if = "Vec::is_empty")]
74    pub system_prompt_overrides: Vec<SystemPromptRule>,
75    #[serde(default, skip_serializing_if = "Option::is_none")]
76    pub bridge_releases: Option<BridgeReleasesSpec>,
77}
78
79/// Release feed for the desktop bridge self-updater.
80///
81/// The bridge cannot reach these assets itself — the repository is private —
82/// so the gateway resolves and proxies them. Keeping the resolution here is
83/// also what makes staged rollouts a config change rather than a client
84/// release.
85#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
86#[serde(deny_unknown_fields)]
87pub struct BridgeReleasesSpec {
88    pub repo: String,
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub token_secret: Option<String>,
91    #[serde(default = "default_tag_prefix")]
92    pub tag_prefix: String,
93    #[serde(default, skip_serializing_if = "Option::is_none")]
94    pub pinned_version: Option<String>,
95    #[serde(default)]
96    pub assets: std::collections::BTreeMap<String, String>,
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub api_base: Option<String>,
99}
100
101impl BridgeReleasesSpec {
102    #[must_use]
103    pub fn api_base(&self) -> &str {
104        self.api_base.as_deref().unwrap_or("https://api.github.com")
105    }
106}
107
108fn default_tag_prefix() -> String {
109    "bridge-v".to_owned()
110}
111
112impl Default for GatewayConfigSpec {
113    fn default() -> Self {
114        Self {
115            enabled: false,
116            routes: Vec::new(),
117            default_provider: None,
118            default_model: None,
119            allow_unlisted_models: false,
120            quota_fault_mode: QuotaFaultMode::default(),
121            auth_scheme: default_auth_scheme(),
122            inference_path_prefix: default_inference_path_prefix(),
123            system_prompt_overrides: Vec::new(),
124            bridge_releases: None,
125        }
126    }
127}
128
129pub(crate) fn default_auth_scheme() -> String {
130    "bearer".to_owned()
131}
132
133pub(crate) fn default_inference_path_prefix() -> String {
134    "/v1".to_owned()
135}
136
137impl GatewayConfigSpec {
138    #[must_use]
139    pub fn resolve(self) -> GatewayConfig {
140        let Self {
141            enabled,
142            routes,
143            default_provider,
144            default_model,
145            allow_unlisted_models,
146            quota_fault_mode,
147            auth_scheme,
148            inference_path_prefix,
149            system_prompt_overrides,
150            bridge_releases,
151        } = self;
152
153        GatewayConfig {
154            enabled,
155            routes,
156            default_provider,
157            default_model,
158            allow_unlisted_models,
159            quota_fault_mode,
160            auth_scheme,
161            inference_path_prefix,
162            system_prompt_overrides,
163            bridge_releases,
164        }
165    }
166}