Skip to main content

systemprompt_models/profile/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 a
4//! profile; [`GatewayConfig`] is its runtime projection. Routes carry no
5//! embedded provider catalog — every route resolves its provider against
6//! `profile.providers` (`ProviderRegistry`) at use time.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11mod runtime;
12mod validate;
13
14use serde::{Deserialize, Serialize};
15use systemprompt_identifiers::ProviderId;
16
17use crate::profile::gateway::override_rule::SystemPromptRule;
18use crate::profile::gateway::route::GatewayRoute;
19
20pub use runtime::GatewayConfig;
21
22pub(crate) const DEFAULT_ROUTE_PATTERN: &str = "*";
23
24#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct GatewayConfigSpec {
27    #[serde(default)]
28    pub enabled: bool,
29    #[serde(default)]
30    pub routes: Vec<GatewayRoute>,
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub default_provider: Option<ProviderId>,
33    // Why: advertised over `GET /v1/bridge/profile`, so changing it here moves
34    // the fleet default without shipping a new bridge build.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub default_model: Option<String>,
37    #[serde(default)]
38    pub allow_unlisted_models: bool,
39    #[serde(default = "default_auth_scheme")]
40    pub auth_scheme: String,
41    #[serde(default = "default_inference_path_prefix")]
42    pub inference_path_prefix: String,
43    #[serde(default, skip_serializing_if = "Vec::is_empty")]
44    pub system_prompt_overrides: Vec<SystemPromptRule>,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub bridge_releases: Option<BridgeReleasesSpec>,
47}
48
49/// Release feed for the desktop bridge self-updater.
50///
51/// The bridge cannot reach these assets itself — the repository is private —
52/// so the gateway resolves and proxies them. Keeping the resolution here is
53/// also what makes staged rollouts a config change rather than a client
54/// release.
55#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
56#[serde(deny_unknown_fields)]
57pub struct BridgeReleasesSpec {
58    pub repo: String,
59    #[serde(default, skip_serializing_if = "Option::is_none")]
60    pub token_env: Option<String>,
61    #[serde(default = "default_tag_prefix")]
62    pub tag_prefix: String,
63    #[serde(default, skip_serializing_if = "Option::is_none")]
64    pub pinned_version: Option<String>,
65    #[serde(default)]
66    pub assets: std::collections::BTreeMap<String, String>,
67    // Why: the GitHub API host is a field rather than a constant so the
68    // release routes can be pointed at a stub. Hardcoded, every line past
69    // "is this configured" needed a real call to api.github.com to reach,
70    // which is not something a test can do. Absent means the real host, so
71    // no deployment has to know this exists.
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub api_base: Option<String>,
74}
75
76impl BridgeReleasesSpec {
77    #[must_use]
78    pub fn api_base(&self) -> &str {
79        self.api_base.as_deref().unwrap_or("https://api.github.com")
80    }
81}
82
83fn default_tag_prefix() -> String {
84    "bridge-v".to_owned()
85}
86
87impl Default for GatewayConfigSpec {
88    fn default() -> Self {
89        Self {
90            enabled: false,
91            routes: Vec::new(),
92            default_provider: None,
93            default_model: None,
94            allow_unlisted_models: false,
95            auth_scheme: default_auth_scheme(),
96            inference_path_prefix: default_inference_path_prefix(),
97            system_prompt_overrides: Vec::new(),
98            bridge_releases: None,
99        }
100    }
101}
102
103pub(crate) fn default_auth_scheme() -> String {
104    "bearer".to_owned()
105}
106
107pub(crate) fn default_inference_path_prefix() -> String {
108    "/v1".to_owned()
109}
110
111impl GatewayConfigSpec {
112    #[must_use]
113    pub fn resolve(self) -> GatewayConfig {
114        let Self {
115            enabled,
116            routes,
117            default_provider,
118            default_model,
119            allow_unlisted_models,
120            auth_scheme,
121            inference_path_prefix,
122            system_prompt_overrides,
123            bridge_releases,
124        } = self;
125
126        GatewayConfig {
127            enabled,
128            routes,
129            default_provider,
130            default_model,
131            allow_unlisted_models,
132            auth_scheme,
133            inference_path_prefix,
134            system_prompt_overrides,
135            bridge_releases,
136        }
137    }
138}