1use sim_citizen_derive::non_citizen;
2use sim_kernel::{ContentId, Cx, Expr, Object, ObjectCompat, Result, Symbol};
3use sim_lib_net_core::hex_encode;
4
5pub const GATEWAY_REQUEST_OBJECT: &str = "openai-gateway/request";
7pub const GATEWAY_RESPONSE_OBJECT: &str = "openai-gateway/response";
9pub const GATEWAY_RUN_OBJECT: &str = "openai-gateway/run";
11pub const GATEWAY_EVENT_OBJECT: &str = "openai-gateway/event";
13
14#[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 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 pub fn get(path: impl Into<String>) -> Self {
54 Self::new("GET", path, Vec::new(), Vec::new())
55 }
56
57 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 pub fn id(&self) -> Option<&str> {
67 self.id.as_deref()
68 }
69
70 pub fn timestamp_ms(&self) -> Option<u64> {
72 self.timestamp_ms
73 }
74
75 pub fn method(&self) -> &str {
77 &self.method
78 }
79
80 pub fn path(&self) -> &str {
82 &self.path
83 }
84
85 pub fn headers(&self) -> &[(String, String)] {
87 &self.headers
88 }
89
90 pub fn body(&self) -> &[u8] {
92 &self.body
93 }
94
95 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#[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 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 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 pub fn json_value(status: u16, body: serde_json::Value) -> Self {
166 Self::json(status, canonical_json_bytes(body))
167 }
168
169 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 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 pub fn status(&self) -> u16 {
189 self.status
190 }
191
192 pub fn headers(&self) -> &[(String, String)] {
194 &self.headers
195 }
196
197 pub fn body(&self) -> &[u8] {
199 &self.body
200 }
201
202 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 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#[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 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 pub fn with_status(mut self, status: impl Into<Symbol>) -> Self {
275 self.status = status.into();
276 self
277 }
278
279 pub fn id(&self) -> &str {
281 &self.id
282 }
283
284 pub fn request_content_id(&self) -> &ContentId {
286 &self.request_content_id
287 }
288
289 pub fn status(&self) -> &Symbol {
291 &self.status
292 }
293
294 pub fn created_at_ms(&self) -> u64 {
296 self.created_at_ms
297 }
298
299 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#[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 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 pub fn id(&self) -> &str {
376 &self.id
377 }
378
379 pub fn run_id(&self) -> &str {
381 &self.run_id
382 }
383
384 pub fn sequence(&self) -> u64 {
386 self.sequence
387 }
388
389 pub fn kind(&self) -> &Symbol {
391 &self.kind
392 }
393
394 pub fn payload(&self) -> &Expr {
396 &self.payload
397 }
398
399 pub fn created_at_ms(&self) -> u64 {
401 self.created_at_ms
402 }
403
404 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#[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 pub fn new(response: GatewayResponse) -> Self {
457 Self { response }
458 }
459
460 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
482pub 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
492pub 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;