Skip to main content

sim_lib_openai_server/
objects.rs

1use sim_citizen_derive::non_citizen;
2use sim_kernel::{ContentId, Cx, Expr, Object, ObjectCompat, Result, Symbol};
3use sim_lib_net_core::hex_encode;
4
5/// Object-kind tag identifying a [`GatewayRequest`] in its `Expr` projection.
6pub const GATEWAY_REQUEST_OBJECT: &str = "openai-gateway/request";
7/// Object-kind tag identifying a [`GatewayResponse`] in its `Expr` projection.
8pub const GATEWAY_RESPONSE_OBJECT: &str = "openai-gateway/response";
9/// Object-kind tag identifying a [`GatewayRun`] in its `Expr` projection.
10pub const GATEWAY_RUN_OBJECT: &str = "openai-gateway/run";
11/// Object-kind tag identifying a [`GatewayEvent`] in its `Expr` projection.
12pub const GATEWAY_EVENT_OBJECT: &str = "openai-gateway/event";
13
14/// Represents an inbound HTTP request to the OpenAI-shaped gateway.
15///
16/// Carries the request line (method and path), headers, and raw body, plus
17/// optional gateway-assigned metadata (an id and a receipt timestamp).
18#[non_citizen(
19    reason = "gateway request runtime shell; class-backed descriptor is openai/GatewayRequest",
20    kind = "marker",
21    descriptor = "openai/GatewayRequest"
22)]
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub struct GatewayRequest {
25    id: Option<String>,
26    timestamp_ms: Option<u64>,
27    method: String,
28    path: String,
29    headers: Vec<(String, String)>,
30    body: Vec<u8>,
31}
32
33impl GatewayRequest {
34    /// Builds a request from a method, path, headers, and body, with no id or
35    /// timestamp metadata attached yet.
36    pub fn new(
37        method: impl Into<String>,
38        path: impl Into<String>,
39        headers: Vec<(String, String)>,
40        body: Vec<u8>,
41    ) -> Self {
42        Self {
43            id: None,
44            timestamp_ms: None,
45            method: method.into(),
46            path: path.into(),
47            headers,
48            body,
49        }
50    }
51
52    /// Builds a `GET` request for the given path with empty headers and body.
53    pub fn get(path: impl Into<String>) -> Self {
54        Self::new("GET", path, Vec::new(), Vec::new())
55    }
56
57    /// Attaches a gateway-assigned id and receipt timestamp, returning the
58    /// updated request.
59    pub fn with_metadata(mut self, id: impl Into<String>, timestamp_ms: u64) -> Self {
60        self.id = Some(id.into());
61        self.timestamp_ms = Some(timestamp_ms);
62        self
63    }
64
65    /// Returns the gateway-assigned id, or `None` if no metadata is attached.
66    pub fn id(&self) -> Option<&str> {
67        self.id.as_deref()
68    }
69
70    /// Returns the receipt timestamp in milliseconds, or `None` if unset.
71    pub fn timestamp_ms(&self) -> Option<u64> {
72        self.timestamp_ms
73    }
74
75    /// Returns the HTTP method (for example `GET` or `POST`).
76    pub fn method(&self) -> &str {
77        &self.method
78    }
79
80    /// Returns the request path.
81    pub fn path(&self) -> &str {
82        &self.path
83    }
84
85    /// Returns the request headers as name/value pairs.
86    pub fn headers(&self) -> &[(String, String)] {
87        &self.headers
88    }
89
90    /// Returns the raw request body bytes.
91    pub fn body(&self) -> &[u8] {
92        &self.body
93    }
94
95    /// Projects the request into its canonical `Expr` map representation.
96    pub fn to_expr(&self) -> Expr {
97        Expr::Map(vec![
98            field("object", Expr::String(GATEWAY_REQUEST_OBJECT.to_owned())),
99            optional_string_field("id", self.id.as_deref()),
100            optional_u64_field("timestamp-ms", self.timestamp_ms),
101            field("method", Expr::String(self.method.clone())),
102            field("path", Expr::String(self.path.clone())),
103            field("headers", headers_expr(&self.headers)),
104            field("body", Expr::Bytes(self.body.clone())),
105        ])
106    }
107}
108
109impl Object for GatewayRequest {
110    fn display(&self, _cx: &mut Cx) -> Result<String> {
111        Ok(format!(
112            "#<openai-gateway-request {} {}>",
113            self.method, self.path
114        ))
115    }
116
117    fn as_any(&self) -> &dyn std::any::Any {
118        self
119    }
120}
121
122impl ObjectCompat for GatewayRequest {
123    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
124        Ok(self.to_expr())
125    }
126}
127
128/// Represents an HTTP response produced by the gateway.
129///
130/// Pairs a status code with response headers and a raw body. Constructors such
131/// as [`GatewayResponse::json`], [`GatewayResponse::text`], and
132/// [`GatewayResponse::sse`] preset the `Content-Type` header.
133#[non_citizen(
134    reason = "gateway response runtime shell; class-backed descriptor is openai/GatewayResponse",
135    kind = "marker",
136    descriptor = "openai/GatewayResponse"
137)]
138#[derive(Clone, Debug, PartialEq, Eq)]
139pub struct GatewayResponse {
140    status: u16,
141    headers: Vec<(String, String)>,
142    body: Vec<u8>,
143}
144
145impl GatewayResponse {
146    /// Builds a response from an explicit status, headers, and body.
147    pub fn new(status: u16, headers: Vec<(String, String)>, body: Vec<u8>) -> Self {
148        Self {
149            status,
150            headers,
151            body,
152        }
153    }
154
155    /// Builds a response with `Content-Type: application/json`.
156    pub fn json(status: u16, body: Vec<u8>) -> Self {
157        Self::new(
158            status,
159            vec![("Content-Type".to_owned(), "application/json".to_owned())],
160            body,
161        )
162    }
163
164    /// Builds a JSON response with recursively canonicalized object-key order.
165    pub fn json_value(status: u16, body: serde_json::Value) -> Self {
166        Self::json(status, canonical_json_bytes(body))
167    }
168
169    /// Builds a response with `Content-Type: text/plain`.
170    pub fn text(status: u16, body: impl Into<Vec<u8>>) -> Self {
171        Self::new(
172            status,
173            vec![("Content-Type".to_owned(), "text/plain".to_owned())],
174            body.into(),
175        )
176    }
177
178    /// Builds a streaming response with `Content-Type: text/event-stream`.
179    pub fn sse(status: u16, body: impl Into<Vec<u8>>) -> Self {
180        Self::new(
181            status,
182            vec![("Content-Type".to_owned(), "text/event-stream".to_owned())],
183            body.into(),
184        )
185    }
186
187    /// Returns the HTTP status code.
188    pub fn status(&self) -> u16 {
189        self.status
190    }
191
192    /// Returns the response headers as name/value pairs.
193    pub fn headers(&self) -> &[(String, String)] {
194        &self.headers
195    }
196
197    /// Returns the raw response body bytes.
198    pub fn body(&self) -> &[u8] {
199        &self.body
200    }
201
202    /// Returns the first header value matching `name` case-insensitively, or
203    /// `None` if no such header is present.
204    pub fn header(&self, name: &str) -> Option<&str> {
205        self.headers
206            .iter()
207            .find(|(key, _)| key.eq_ignore_ascii_case(name))
208            .map(|(_, value)| value.as_str())
209    }
210
211    /// Projects the response into its canonical `Expr` map representation.
212    pub fn to_expr(&self) -> Expr {
213        Expr::Map(vec![
214            field("object", Expr::String(GATEWAY_RESPONSE_OBJECT.to_owned())),
215            field("status", Expr::String(self.status.to_string())),
216            field("headers", headers_expr(&self.headers)),
217            field("body", Expr::Bytes(self.body.clone())),
218        ])
219    }
220}
221
222pub(crate) fn canonical_json_bytes(mut value: serde_json::Value) -> Vec<u8> {
223    value.sort_all_objects();
224    serde_json::to_vec(&value).expect("serializing a JSON value cannot fail")
225}
226
227impl Object for GatewayResponse {
228    fn display(&self, _cx: &mut Cx) -> Result<String> {
229        Ok(format!("#<openai-gateway-response {}>", self.status))
230    }
231
232    fn as_any(&self) -> &dyn std::any::Any {
233        self
234    }
235}
236
237impl ObjectCompat for GatewayResponse {
238    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
239        Ok(self.to_expr())
240    }
241}
242
243/// Represents a single gateway run: one accepted request being processed.
244///
245/// A run is keyed by its own id and the content id of the originating request,
246/// tracks a lifecycle [`Symbol`] status (starting at `created`), and records
247/// when it was created.
248#[non_citizen(
249    reason = "gateway run runtime shell; class-backed descriptor is openai/GatewayRun",
250    kind = "marker",
251    descriptor = "openai/GatewayRun"
252)]
253#[derive(Clone, Debug, PartialEq, Eq)]
254pub struct GatewayRun {
255    id: String,
256    request_content_id: ContentId,
257    status: Symbol,
258    created_at_ms: u64,
259}
260
261impl GatewayRun {
262    /// Builds a run in the `created` state for the given id, originating
263    /// request content id, and creation timestamp.
264    pub fn new(id: impl Into<String>, request_content_id: ContentId, created_at_ms: u64) -> Self {
265        Self {
266            id: id.into(),
267            request_content_id,
268            status: Symbol::new("created"),
269            created_at_ms,
270        }
271    }
272
273    /// Returns the run with its lifecycle status replaced by `status`.
274    pub fn with_status(mut self, status: impl Into<Symbol>) -> Self {
275        self.status = status.into();
276        self
277    }
278
279    /// Returns the run id.
280    pub fn id(&self) -> &str {
281        &self.id
282    }
283
284    /// Returns the content id of the request that initiated this run.
285    pub fn request_content_id(&self) -> &ContentId {
286        &self.request_content_id
287    }
288
289    /// Returns the current lifecycle status symbol.
290    pub fn status(&self) -> &Symbol {
291        &self.status
292    }
293
294    /// Returns the creation timestamp in milliseconds.
295    pub fn created_at_ms(&self) -> u64 {
296        self.created_at_ms
297    }
298
299    /// Projects the run into its canonical `Expr` map representation.
300    pub fn to_expr(&self) -> Expr {
301        Expr::Map(vec![
302            field("object", Expr::String(GATEWAY_RUN_OBJECT.to_owned())),
303            field("id", Expr::String(self.id.clone())),
304            field(
305                "request-content-id",
306                content_id_expr(&self.request_content_id),
307            ),
308            field("status", Expr::Symbol(self.status.clone())),
309            field(
310                "created-at-ms",
311                Expr::String(self.created_at_ms.to_string()),
312            ),
313        ])
314    }
315}
316
317impl Object for GatewayRun {
318    fn display(&self, _cx: &mut Cx) -> Result<String> {
319        Ok(format!("#<openai-gateway-run {}>", self.id))
320    }
321
322    fn as_any(&self) -> &dyn std::any::Any {
323        self
324    }
325}
326
327impl ObjectCompat for GatewayRun {
328    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
329        Ok(self.to_expr())
330    }
331}
332
333/// Represents one event emitted during a [`GatewayRun`].
334///
335/// Each event has its own id, the id of the run it belongs to, a monotonic
336/// `sequence` within that run, a [`Symbol`] kind, an arbitrary `Expr` payload,
337/// and a creation timestamp.
338#[non_citizen(
339    reason = "gateway event runtime shell; class-backed descriptor is openai/GatewayEvent",
340    kind = "marker",
341    descriptor = "openai/GatewayEvent"
342)]
343#[derive(Clone, Debug, PartialEq, Eq)]
344pub struct GatewayEvent {
345    id: String,
346    run_id: String,
347    sequence: u64,
348    kind: Symbol,
349    payload: Expr,
350    created_at_ms: u64,
351}
352
353impl GatewayEvent {
354    /// Builds an event for a run from its id, parent run id, sequence number,
355    /// kind, payload, and creation timestamp.
356    pub fn new(
357        id: impl Into<String>,
358        run_id: impl Into<String>,
359        sequence: u64,
360        kind: impl Into<Symbol>,
361        payload: Expr,
362        created_at_ms: u64,
363    ) -> Self {
364        Self {
365            id: id.into(),
366            run_id: run_id.into(),
367            sequence,
368            kind: kind.into(),
369            payload,
370            created_at_ms,
371        }
372    }
373
374    /// Returns the event id.
375    pub fn id(&self) -> &str {
376        &self.id
377    }
378
379    /// Returns the id of the run this event belongs to.
380    pub fn run_id(&self) -> &str {
381        &self.run_id
382    }
383
384    /// Returns the event's sequence number within its run.
385    pub fn sequence(&self) -> u64 {
386        self.sequence
387    }
388
389    /// Returns the event kind symbol.
390    pub fn kind(&self) -> &Symbol {
391        &self.kind
392    }
393
394    /// Returns the event payload expression.
395    pub fn payload(&self) -> &Expr {
396        &self.payload
397    }
398
399    /// Returns the creation timestamp in milliseconds.
400    pub fn created_at_ms(&self) -> u64 {
401        self.created_at_ms
402    }
403
404    /// Projects the event into its canonical `Expr` map representation.
405    pub fn to_expr(&self) -> Expr {
406        Expr::Map(vec![
407            field("object", Expr::String(GATEWAY_EVENT_OBJECT.to_owned())),
408            field("id", Expr::String(self.id.clone())),
409            field("run-id", Expr::String(self.run_id.clone())),
410            field("sequence", Expr::String(self.sequence.to_string())),
411            field("event-kind", Expr::Symbol(self.kind.clone())),
412            field("payload", self.payload.clone()),
413            field(
414                "created-at-ms",
415                Expr::String(self.created_at_ms.to_string()),
416            ),
417        ])
418    }
419}
420
421impl Object for GatewayEvent {
422    fn display(&self, _cx: &mut Cx) -> Result<String> {
423        Ok(format!(
424            "#<openai-gateway-event {} {}>",
425            self.run_id, self.sequence
426        ))
427    }
428
429    fn as_any(&self) -> &dyn std::any::Any {
430        self
431    }
432}
433
434impl ObjectCompat for GatewayEvent {
435    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
436        Ok(self.to_expr())
437    }
438}
439
440/// Wraps a [`GatewayResponse`] as a runtime [`Object`] value.
441///
442/// This is the handle returned to the runtime; its serializable `Expr`
443/// projection delegates to the wrapped response.
444#[derive(Clone)]
445#[non_citizen(
446    reason = "runtime response wrapper; serializable projection is openai/GatewayResponse descriptor",
447    kind = "handle",
448    descriptor = "openai/GatewayResponse"
449)]
450pub struct GatewayResponseValue {
451    response: GatewayResponse,
452}
453
454impl GatewayResponseValue {
455    /// Wraps the given response as a runtime value.
456    pub fn new(response: GatewayResponse) -> Self {
457        Self { response }
458    }
459
460    /// Returns a reference to the wrapped response.
461    pub fn response(&self) -> &GatewayResponse {
462        &self.response
463    }
464}
465
466impl Object for GatewayResponseValue {
467    fn display(&self, cx: &mut Cx) -> Result<String> {
468        self.response.display(cx)
469    }
470
471    fn as_any(&self) -> &dyn std::any::Any {
472        self
473    }
474}
475
476impl ObjectCompat for GatewayResponseValue {
477    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
478        Ok(self.response.to_expr())
479    }
480}
481
482/// Projects a [`ContentId`] into an `Expr` map with its algorithm, raw bytes,
483/// and hex-encoded digest.
484pub fn content_id_expr(id: &ContentId) -> Expr {
485    Expr::Map(vec![
486        field("algorithm", Expr::Symbol(id.algorithm.clone())),
487        field("bytes", Expr::Bytes(id.bytes.to_vec())),
488        field("hex", Expr::String(hex_encode(&id.bytes))),
489    ])
490}
491
492/// Returns the lowercase hex encoding of a [`ContentId`]'s digest bytes.
493pub fn content_id_hex(id: &ContentId) -> String {
494    hex_encode(&id.bytes)
495}
496
497fn headers_expr(headers: &[(String, String)]) -> Expr {
498    let mut sorted = headers.to_vec();
499    sorted.sort_by_key(|(name, value)| (name.to_ascii_lowercase(), value.clone()));
500    Expr::List(
501        sorted
502            .into_iter()
503            .map(|(name, value)| {
504                Expr::Map(vec![
505                    field("name", Expr::String(name)),
506                    field("value", Expr::String(value)),
507                ])
508            })
509            .collect(),
510    )
511}
512
513fn optional_string_field(name: &str, value: Option<&str>) -> (Expr, Expr) {
514    field(
515        name,
516        value
517            .map(|value| Expr::String(value.to_owned()))
518            .unwrap_or(Expr::Nil),
519    )
520}
521
522fn optional_u64_field(name: &str, value: Option<u64>) -> (Expr, Expr) {
523    field(
524        name,
525        value
526            .map(|value| Expr::String(value.to_string()))
527            .unwrap_or(Expr::Nil),
528    )
529}
530
531use sim_value::build::entry as field;