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 /// Which subject the bucket is keyed by: `user` (the default), or any
20 /// subject-attribute dimension registered by an extension (for example
21 /// `organization`). A window whose subject cannot be resolved for the
22 /// requesting user is skipped.
23 #[serde(default = "default_subject")]
24 pub subject: String,
25 pub max_requests: Option<i64>,
26 pub max_input_tokens: Option<i64>,
27 pub max_output_tokens: Option<i64>,
28 /// Spend ceiling for the window. Enforced one request late: cost is known
29 /// only after the response, so the request that crosses the ceiling
30 /// completes and subsequent requests are denied.
31 #[serde(default)]
32 pub max_cost_microdollars: Option<i64>,
33}
34
35impl Default for QuotaWindow {
36 fn default() -> Self {
37 Self {
38 window_seconds: 0,
39 subject: default_subject(),
40 max_requests: None,
41 max_input_tokens: None,
42 max_output_tokens: None,
43 max_cost_microdollars: None,
44 }
45 }
46}
47
48fn default_subject() -> String {
49 "user".to_owned()
50}
51
52pub const USER_QUOTA_SUBJECT: &str = "user";
53
54/// How far back into a conversation the request-phase scanners look.
55///
56/// A request carries the whole conversation, so scanning all of it re-reads
57/// every earlier turn on every turn: one finding would deny the rest of the
58/// conversation, and each turn would persist the same finding again.
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
60#[serde(rename_all = "lowercase")]
61pub enum SafetyHistoryMode {
62 /// Judge only the newest user turn.
63 #[default]
64 Off,
65 /// Also scan earlier turns, recording findings at phase `request_history`
66 /// without denying the request.
67 Audit,
68 /// Scan earlier turns and let their findings deny the request.
69 Block,
70}
71
72/// Phrase-list tuning for the builtin `heuristic` scanner. Ignored when an
73/// extension registers its own scanner under the name `heuristic`, which
74/// shadows the builtin wholesale.
75#[derive(Debug, Clone, Serialize, Deserialize, Default)]
76#[serde(deny_unknown_fields)]
77pub struct HeuristicConfig {
78 /// Replaces the builtin phrase list entirely when set.
79 #[serde(default)]
80 pub phrases: Option<Vec<String>>,
81 /// Appended to whichever base list is in effect.
82 #[serde(default)]
83 pub extra_phrases: Vec<String>,
84 /// Drops the builtin list, leaving only `phrases`/`extra_phrases`.
85 #[serde(default)]
86 pub disable_builtin: bool,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, Default)]
90#[serde(deny_unknown_fields)]
91pub struct SafetyConfig {
92 #[serde(default)]
93 pub scanners: Vec<String>,
94 #[serde(default)]
95 pub heuristic: HeuristicConfig,
96 #[serde(default)]
97 pub block_categories: Vec<String>,
98 /// Response-phase categories that deny the reply instead of merely
99 /// recording it. Buffered replies only: a streamed reply is already on the
100 /// wire by the time the scan can run, so listing a category here has no
101 /// effect on streaming and the gateway does not pretend otherwise.
102 #[serde(default)]
103 pub block_response_categories: Vec<String>,
104 #[serde(default)]
105 pub history: SafetyHistoryMode,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize, Default)]
109#[serde(deny_unknown_fields)]
110pub struct GatewayPolicySpec {
111 #[serde(default)]
112 pub quota_windows: Vec<QuotaWindow>,
113 #[serde(default)]
114 pub safety: SafetyConfig,
115}
116
117impl GatewayPolicySpec {
118 #[must_use]
119 pub fn permissive() -> Self {
120 Self::default()
121 }
122}