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 response with `Content-Type: text/plain`.
165    pub fn text(status: u16, body: impl Into<Vec<u8>>) -> Self {
166        Self::new(
167            status,
168            vec![("Content-Type".to_owned(), "text/plain".to_owned())],
169            body.into(),
170        )
171    }
172
173    /// Builds a streaming response with `Content-Type: text/event-stream`.
174    pub fn sse(status: u16, body: impl Into<Vec<u8>>) -> Self {
175        Self::new(
176            status,
177            vec![("Content-Type".to_owned(), "text/event-stream".to_owned())],
178            body.into(),
179        )
180    }
181
182    /// Returns the HTTP status code.
183    pub fn status(&self) -> u16 {
184        self.status
185    }
186
187    /// Returns the response headers as name/value pairs.
188    pub fn headers(&self) -> &[(String, String)] {
189        &self.headers
190    }
191
192    /// Returns the raw response body bytes.
193    pub fn body(&self) -> &[u8] {
194        &self.body
195    }
196
197    /// Returns the first header value matching `name` case-insensitively, or
198    /// `None` if no such header is present.
199    pub fn header(&self, name: &str) -> Option<&str> {
200        self.headers
201            .iter()
202            .find(|(key, _)| key.eq_ignore_ascii_case(name))
203            .map(|(_, value)| value.as_str())
204    }
205
206    /// Projects the response into its canonical `Expr` map representation.
207    pub fn to_expr(&self) -> Expr {
208        Expr::Map(vec![
209            field("object", Expr::String(GATEWAY_RESPONSE_OBJECT.to_owned())),
210            field("status", Expr::String(self.status.to_string())),
211            field("headers", headers_expr(&self.headers)),
212            field("body", Expr::Bytes(self.body.clone())),
213        ])
214    }
215}
216
217impl Object for GatewayResponse {
218    fn display(&self, _cx: &mut Cx) -> Result<String> {
219        Ok(format!("#<openai-gateway-response {}>", self.status))
220    }
221
222    fn as_any(&self) -> &dyn std::any::Any {
223        self
224    }
225}
226
227impl ObjectCompat for GatewayResponse {
228    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
229        Ok(self.to_expr())
230    }
231}
232
233/// Represents a single gateway run: one accepted request being processed.
234///
235/// A run is keyed by its own id and the content id of the originating request,
236/// tracks a lifecycle [`Symbol`] status (starting at `created`), and records
237/// when it was created.
238#[non_citizen(
239    reason = "gateway run runtime shell; class-backed descriptor is openai/GatewayRun",
240    kind = "marker",
241    descriptor = "openai/GatewayRun"
242)]
243#[derive(Clone, Debug, PartialEq, Eq)]
244pub struct GatewayRun {
245    id: String,
246    request_content_id: ContentId,
247    status: Symbol,
248    created_at_ms: u64,
249}
250
251impl GatewayRun {
252    /// Builds a run in the `created` state for the given id, originating
253    /// request content id, and creation timestamp.
254    pub fn new(id: impl Into<String>, request_content_id: ContentId, created_at_ms: u64) -> Self {
255        Self {
256            id: id.into(),
257            request_content_id,
258            status: Symbol::new("created"),
259            created_at_ms,
260        }
261    }
262
263    /// Returns the run with its lifecycle status replaced by `status`.
264    pub fn with_status(mut self, status: impl Into<Symbol>) -> Self {
265        self.status = status.into();
266        self
267    }
268
269    /// Returns the run id.
270    pub fn id(&self) -> &str {
271        &self.id
272    }
273
274    /// Returns the content id of the request that initiated this run.
275    pub fn request_content_id(&self) -> &ContentId {
276        &self.request_content_id
277    }
278
279    /// Returns the current lifecycle status symbol.
280    pub fn status(&self) -> &Symbol {
281        &self.status
282    }
283
284    /// Returns the creation timestamp in milliseconds.
285    pub fn created_at_ms(&self) -> u64 {
286        self.created_at_ms
287    }
288
289    /// Projects the run into its canonical `Expr` map representation.
290    pub fn to_expr(&self) -> Expr {
291        Expr::Map(vec![
292            field("object", Expr::String(GATEWAY_RUN_OBJECT.to_owned())),
293            field("id", Expr::String(self.id.clone())),
294            field(
295                "request-content-id",
296                content_id_expr(&self.request_content_id),
297            ),
298            field("status", Expr::Symbol(self.status.clone())),
299            field(
300                "created-at-ms",
301                Expr::String(self.created_at_ms.to_string()),
302            ),
303        ])
304    }
305}
306
307impl Object for GatewayRun {
308    fn display(&self, _cx: &mut Cx) -> Result<String> {
309        Ok(format!("#<openai-gateway-run {}>", self.id))
310    }
311
312    fn as_any(&self) -> &dyn std::any::Any {
313        self
314    }
315}
316
317impl ObjectCompat for GatewayRun {
318    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
319        Ok(self.to_expr())
320    }
321}
322
323/// Represents one event emitted during a [`GatewayRun`].
324///
325/// Each event has its own id, the id of the run it belongs to, a monotonic
326/// `sequence` within that run, a [`Symbol`] kind, an arbitrary `Expr` payload,
327/// and a creation timestamp.
328#[non_citizen(
329    reason = "gateway event runtime shell; class-backed descriptor is openai/GatewayEvent",
330    kind = "marker",
331    descriptor = "openai/GatewayEvent"
332)]
333#[derive(Clone, Debug, PartialEq, Eq)]
334pub struct GatewayEvent {
335    id: String,
336    run_id: String,
337    sequence: u64,
338    kind: Symbol,
339    payload: Expr,
340    created_at_ms: u64,
341}
342
343impl GatewayEvent {
344    /// Builds an event for a run from its id, parent run id, sequence number,
345    /// kind, payload, and creation timestamp.
346    pub fn new(
347        id: impl Into<String>,
348        run_id: impl Into<String>,
349        sequence: u64,
350        kind: impl Into<Symbol>,
351        payload: Expr,
352        created_at_ms: u64,
353    ) -> Self {
354        Self {
355            id: id.into(),
356            run_id: run_id.into(),
357            sequence,
358            kind: kind.into(),
359            payload,
360            created_at_ms,
361        }
362    }
363
364    /// Returns the event id.
365    pub fn id(&self) -> &str {
366        &self.id
367    }
368
369    /// Returns the id of the run this event belongs to.
370    pub fn run_id(&self) -> &str {
371        &self.run_id
372    }
373
374    /// Returns the event's sequence number within its run.
375    pub fn sequence(&self) -> u64 {
376        self.sequence
377    }
378
379    /// Returns the event kind symbol.
380    pub fn kind(&self) -> &Symbol {
381        &self.kind
382    }
383
384    /// Returns the event payload expression.
385    pub fn payload(&self) -> &Expr {
386        &self.payload
387    }
388
389    /// Returns the creation timestamp in milliseconds.
390    pub fn created_at_ms(&self) -> u64 {
391        self.created_at_ms
392    }
393
394    /// Projects the event into its canonical `Expr` map representation.
395    pub fn to_expr(&self) -> Expr {
396        Expr::Map(vec![
397            field("object", Expr::String(GATEWAY_EVENT_OBJECT.to_owned())),
398            field("id", Expr::String(self.id.clone())),
399            field("run-id", Expr::String(self.run_id.clone())),
400            field("sequence", Expr::String(self.sequence.to_string())),
401            field("event-kind", Expr::Symbol(self.kind.clone())),
402            field("payload", self.payload.clone()),
403            field(
404                "created-at-ms",
405                Expr::String(self.created_at_ms.to_string()),
406            ),
407        ])
408    }
409}
410
411impl Object for GatewayEvent {
412    fn display(&self, _cx: &mut Cx) -> Result<String> {
413        Ok(format!(
414            "#<openai-gateway-event {} {}>",
415            self.run_id, self.sequence
416        ))
417    }
418
419    fn as_any(&self) -> &dyn std::any::Any {
420        self
421    }
422}
423
424impl ObjectCompat for GatewayEvent {
425    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
426        Ok(self.to_expr())
427    }
428}
429
430/// Wraps a [`GatewayResponse`] as a runtime [`Object`] value.
431///
432/// This is the handle returned to the runtime; its serializable `Expr`
433/// projection delegates to the wrapped response.
434#[derive(Clone)]
435#[non_citizen(
436    reason = "runtime response wrapper; serializable projection is openai/GatewayResponse descriptor",
437    kind = "handle",
438    descriptor = "openai/GatewayResponse"
439)]
440pub struct GatewayResponseValue {
441    response: GatewayResponse,
442}
443
444impl GatewayResponseValue {
445    /// Wraps the given response as a runtime value.
446    pub fn new(response: GatewayResponse) -> Self {
447        Self { response }
448    }
449
450    /// Returns a reference to the wrapped response.
451    pub fn response(&self) -> &GatewayResponse {
452        &self.response
453    }
454}
455
456impl Object for GatewayResponseValue {
457    fn display(&self, cx: &mut Cx) -> Result<String> {
458        self.response.display(cx)
459    }
460
461    fn as_any(&self) -> &dyn std::any::Any {
462        self
463    }
464}
465
466impl ObjectCompat for GatewayResponseValue {
467    fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
468        Ok(self.response.to_expr())
469    }
470}
471
472/// Projects a [`ContentId`] into an `Expr` map with its algorithm, raw bytes,
473/// and hex-encoded digest.
474pub fn content_id_expr(id: &ContentId) -> Expr {
475    Expr::Map(vec![
476        field("algorithm", Expr::Symbol(id.algorithm.clone())),
477        field("bytes", Expr::Bytes(id.bytes.to_vec())),
478        field("hex", Expr::String(hex_encode(&id.bytes))),
479    ])
480}
481
482/// Returns the lowercase hex encoding of a [`ContentId`]'s digest bytes.
483pub fn content_id_hex(id: &ContentId) -> String {
484    hex_encode(&id.bytes)
485}
486
487fn headers_expr(headers: &[(String, String)]) -> Expr {
488    let mut sorted = headers.to_vec();
489    sorted.sort_by_key(|(name, value)| (name.to_ascii_lowercase(), value.clone()));
490    Expr::List(
491        sorted
492            .into_iter()
493            .map(|(name, value)| {
494                Expr::Map(vec![
495                    field("name", Expr::String(name)),
496                    field("value", Expr::String(value)),
497                ])
498            })
499            .collect(),
500    )
501}
502
503fn optional_string_field(name: &str, value: Option<&str>) -> (Expr, Expr) {
504    field(
505        name,
506        value
507            .map(|value| Expr::String(value.to_owned()))
508            .unwrap_or(Expr::Nil),
509    )
510}
511
512fn optional_u64_field(name: &str, value: Option<u64>) -> (Expr, Expr) {
513    field(
514        name,
515        value
516            .map(|value| Expr::String(value.to_string()))
517            .unwrap_or(Expr::Nil),
518    )
519}
520
521use sim_value::build::entry as field;