Skip to main content

systemprompt_ai/services/gateway/
spec.rs

1//! Declarative gateway-policy specification.
2//!
3//! Spec payload of `ai_gateway_policies` rows, shared with the YAML schema in
4//! `services/gateway/policies.yaml`. Carries quota windows and safety
5//! configuration.
6//!
7//! Model exposure lives on the profile's gateway catalog, not here — see
8//! `GatewayConfig::is_model_exposed`.
9//!
10//! Copyright (c) systemprompt.io — Business Source License 1.1.
11//! See <https://systemprompt.io> for licensing details.
12
13use serde::{Deserialize, Serialize};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct QuotaWindow {
18    pub window_seconds: i32,
19    #[serde(default = "default_subject")]
20    pub subject: String,
21    pub max_requests: Option<i64>,
22    pub max_input_tokens: Option<i64>,
23    pub max_output_tokens: Option<i64>,
24    #[serde(default)]
25    pub max_cost_microdollars: Option<i64>,
26}
27
28impl Default for QuotaWindow {
29    fn default() -> Self {
30        Self {
31            window_seconds: 0,
32            subject: default_subject(),
33            max_requests: None,
34            max_input_tokens: None,
35            max_output_tokens: None,
36            max_cost_microdollars: None,
37        }
38    }
39}
40
41fn default_subject() -> String {
42    "user".to_owned()
43}
44
45pub const USER_QUOTA_SUBJECT: &str = "user";
46
47/// How far back into a conversation the request-phase scanners look.
48///
49/// A request carries the whole conversation, so scanning all of it re-reads
50/// every earlier turn on every turn: one finding would deny the rest of the
51/// conversation, and each turn would persist the same finding again.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
53#[serde(rename_all = "lowercase")]
54pub enum SafetyHistoryMode {
55    #[default]
56    Off,
57    Audit,
58    Block,
59}
60
61/// Phrase-list tuning for the builtin `heuristic` scanner.
62#[derive(Debug, Clone, Serialize, Deserialize, Default)]
63#[serde(deny_unknown_fields)]
64pub struct HeuristicConfig {
65    #[serde(default)]
66    pub phrases: Option<Vec<String>>,
67    #[serde(default)]
68    pub extra_phrases: Vec<String>,
69    #[serde(default)]
70    pub disable_builtin: bool,
71}
72
73/// Whether the safety scanners refuse a request or only record what they
74/// found.
75///
76/// `warn` keeps every scanner running and every finding persisted; it only
77/// removes the refusal. It exists so the block lists can be calibrated from
78/// real traffic — a category that never fires and a category that fires on
79/// every developer request look identical until the findings are recorded
80/// without blocking.
81#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
82#[serde(rename_all = "lowercase")]
83pub enum SafetyMode {
84    #[default]
85    Enforce,
86    Warn,
87}
88
89impl SafetyMode {
90    #[must_use]
91    pub const fn is_warn(self) -> bool {
92        matches!(self, Self::Warn)
93    }
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97#[serde(deny_unknown_fields)]
98pub struct SafetyConfig {
99    #[serde(default)]
100    pub mode: SafetyMode,
101    #[serde(default)]
102    pub scanners: Vec<String>,
103    #[serde(default)]
104    pub heuristic: HeuristicConfig,
105    #[serde(default)]
106    pub block_categories: Vec<String>,
107    #[serde(default)]
108    pub block_response_categories: Vec<String>,
109    #[serde(default)]
110    pub history: SafetyHistoryMode,
111}
112
113/// Whether an exhausted quota window refuses the request or only records
114/// that it would have.
115///
116/// The quota windows are the third enforcement plane on an inference request,
117/// beside the governance chain and the safety scanners, and warn mode has to
118/// cover it too or "nothing blocks" is not true. Under `warn` every window is
119/// still reserved against and every ceiling still evaluated; a breach is
120/// written to `governance_decisions` as a `warn` under policy `quota`, and
121/// the request proceeds.
122#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default, PartialEq, Eq)]
123#[serde(rename_all = "lowercase")]
124pub enum QuotaMode {
125    #[default]
126    Enforce,
127    Warn,
128}
129
130impl QuotaMode {
131    #[must_use]
132    pub const fn is_warn(self) -> bool {
133        matches!(self, Self::Warn)
134    }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
138#[serde(deny_unknown_fields)]
139pub struct GatewayPolicySpec {
140    #[serde(default)]
141    pub quota_mode: QuotaMode,
142    #[serde(default)]
143    pub quota_windows: Vec<QuotaWindow>,
144    #[serde(default)]
145    pub safety: SafetyConfig,
146}
147
148impl GatewayPolicySpec {
149    #[must_use]
150    pub fn permissive() -> Self {
151        Self::default()
152    }
153}