Skip to main content

gproxy_protocol/
spec.rs

1//! The OperationSpec registry: every fact about an operation, declared once.
2//!
3//! v2 scattered these facts across 10+ match sites and five parallel
4//! billable lists; here classification, settlement, affinity, and console
5//! metadata all read one declaration. Request-body expectations are not a
6//! field — they derive from the ingress method (single truth).
7
8use http::Method;
9
10use crate::operation::{Operation, OperationKind};
11
12/// One path segment of an ingress pattern. Static tables, no matcher DSL:
13/// the full pattern language is exactly what the known APIs need.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum Seg {
16    /// Literal segment: `v1`, `files`.
17    Lit(&'static str),
18    /// Capture one segment: `{id}`.
19    Param(&'static str),
20    /// Gemini's `models/{model}:generateContent` shape — a capture with a
21    /// literal `:action` suffix in the same segment.
22    ParamAction(&'static str, &'static str),
23    /// Capture the whole remaining path (service-surface prefixes). Only
24    /// valid as the final segment.
25    Rest(&'static str),
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub struct PathPattern(pub &'static [Seg]);
30
31/// How to tell a streaming request from a buffered one at this ingress.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub enum StreamDetect {
34    /// Never streams.
35    Never,
36    /// A boolean body field (`"stream"`) decides; classification promotes
37    /// the operation to its streaming sibling when set.
38    BodyFlag(&'static str),
39    /// A string body field must equal the declared value.
40    BodyValue(&'static str, &'static str),
41    /// A boolean field accepted from JSON or a multipart form field. Media
42    /// endpoints arrive as multipart and cannot be classified through JSON.
43    BodyFlagOrMultipart(&'static str),
44    /// The endpoint itself is the streaming form (`:streamGenerateContent`).
45    Always,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum StreamFraming {
50    Sse,
51    WebSocket,
52    JsonArray,
53}
54
55/// One way this operation enters the proxy.
56#[derive(Debug, Clone, Copy)]
57pub struct Ingress {
58    pub method: &'static Method,
59    pub pattern: PathPattern,
60    pub kind: OperationKind,
61    pub stream: StreamDetect,
62    pub framing: StreamFraming,
63    /// This ingress is a websocket upgrade (`GET /v1/realtime`,
64    /// Responses-over-WS). The engine hands matched upgrades to the WS
65    /// bridge instead of the HTTP path; hosts never hardcode WS routes
66    /// (v2's gateway carried a three-branch if-chain for exactly this).
67    pub upgrade: bool,
68}
69
70/// When the funnel settles this operation.
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum SettleMode {
73    /// Not billable; settles at zero for telemetry only.
74    Free,
75    /// Settle from the response (or stream tail) usage.
76    OnResponse,
77    /// Long-lived session: the setup response carries no usage; settle when
78    /// the trusted server-side observer closes.
79    OnSessionEnd,
80    /// Async-job pattern (video): settle only when the polled body reports
81    /// `status == "completed"`, deduplicated across polls.
82    OnCompletedStatus,
83}
84
85/// Which credential-stickiness the operation needs.
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum Affinity {
88    None,
89    /// Conversation stickiness via session id / fingerprint.
90    Session,
91    /// The named resource (`"file"`, `"video"`) is bound to the credential
92    /// that created it; follow-up calls must land there.
93    Resource(&'static str),
94}
95
96/// Everything the engine needs to know about an operation.
97#[derive(Debug, Clone, Copy)]
98pub struct OperationSpec {
99    pub ingress: &'static [Ingress],
100    pub settle: SettleMode,
101    pub affinity: Affinity,
102}
103
104/// A successful ingress match.
105#[derive(Debug)]
106pub struct Matched {
107    pub operation: Operation,
108    pub kind: OperationKind,
109    pub stream: StreamDetect,
110    pub framing: StreamFraming,
111    pub upgrade: bool,
112    /// Captured `Param`/`ParamAction` values, in pattern order.
113    pub params: Vec<(&'static str, String)>,
114}
115
116pub const fn default_framing(kind: OperationKind, upgrade: bool) -> StreamFraming {
117    if upgrade {
118        return StreamFraming::WebSocket;
119    }
120    match kind {
121        OperationKind::ContentGeneration(
122            crate::operation::ContentGenerationKind::GeminiGenerateContent,
123        ) => StreamFraming::JsonArray,
124        OperationKind::ContentGeneration(
125            crate::operation::ContentGenerationKind::OpenAiResponsesWebSocket,
126        ) => StreamFraming::WebSocket,
127        OperationKind::ContentGeneration(
128            crate::operation::ContentGenerationKind::OpenAiChat
129            | crate::operation::ContentGenerationKind::OpenAiResponses
130            | crate::operation::ContentGenerationKind::ClaudeMessages,
131        )
132        | OperationKind::Family(_) => StreamFraming::Sse,
133    }
134}
135
136/// Streaming promotion: which operation a `BodyFlag` ingress becomes when
137/// the flag is set. Exhaustive so a new streaming pair cannot be missed.
138pub const fn streaming_sibling(operation: Operation) -> Option<Operation> {
139    match operation {
140        Operation::GenerateContent => Some(Operation::StreamGenerateContent),
141        Operation::ListModels
142        | Operation::GetModel
143        | Operation::CountTokens
144        | Operation::SummarizeMemory
145        | Operation::StreamGenerateContent
146        | Operation::GuardianReview
147        | Operation::GuardianClassify
148        | Operation::CompactContent
149        | Operation::CreateConversation
150        | Operation::CreateEmbedding
151        | Operation::BatchCreateEmbedding
152        | Operation::Rerank
153        | Operation::WebSearch
154        | Operation::CreateImage
155        | Operation::EditImage
156        | Operation::CreateSpeech
157        | Operation::CreateTranscription
158        | Operation::CreateTranslation
159        | Operation::CreateFile
160        | Operation::ListFiles
161        | Operation::RetrieveFile
162        | Operation::RetrieveFileContent
163        | Operation::DeleteFile
164        | Operation::CreateVideo
165        | Operation::RetrieveVideo
166        | Operation::ListVideos
167        | Operation::DeleteVideo
168        | Operation::DownloadVideoContent
169        | Operation::RemixVideo
170        | Operation::CreateVideoCharacter
171        | Operation::GetVideoCharacter
172        | Operation::EditVideo
173        | Operation::ExtendVideo
174        | Operation::CreateRealtimeCall
175        | Operation::ConnectRealtime => None,
176    }
177}
178
179impl Operation {
180    /// The one declaration everything reads. Exhaustive: a new operation
181    /// does not compile until its spec exists.
182    pub fn spec(self) -> &'static OperationSpec {
183        crate::specs::spec(self)
184    }
185}