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