1use sim_citizen_derive::non_citizen;
2use sim_kernel::{ContentId, Cx, Expr, Object, ObjectCompat, Result, Symbol};
3
4pub const GATEWAY_REQUEST_OBJECT: &str = "openai-gateway/request";
6pub const GATEWAY_RESPONSE_OBJECT: &str = "openai-gateway/response";
8pub const GATEWAY_RUN_OBJECT: &str = "openai-gateway/run";
10pub const GATEWAY_EVENT_OBJECT: &str = "openai-gateway/event";
12
13#[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 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 pub fn get(path: impl Into<String>) -> Self {
52 Self::new("GET", path, Vec::new(), Vec::new())
53 }
54
55 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 pub fn id(&self) -> Option<&str> {
65 self.id.as_deref()
66 }
67
68 pub fn timestamp_ms(&self) -> Option<u64> {
70 self.timestamp_ms
71 }
72
73 pub fn method(&self) -> &str {
75 &self.method
76 }
77
78 pub fn path(&self) -> &str {
80 &self.path
81 }
82
83 pub fn headers(&self) -> &[(String, String)] {
85 &self.headers
86 }
87
88 pub fn body(&self) -> &[u8] {
90 &self.body
91 }
92
93 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#[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 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 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 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 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 pub fn status(&self) -> u16 {
181 self.status
182 }
183
184 pub fn headers(&self) -> &[(String, String)] {
186 &self.headers
187 }
188
189 pub fn body(&self) -> &[u8] {
191 &self.body
192 }
193
194 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 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#[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 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 pub fn with_status(mut self, status: impl Into<Symbol>) -> Self {
261 self.status = status.into();
262 self
263 }
264
265 pub fn id(&self) -> &str {
267 &self.id
268 }
269
270 pub fn request_content_id(&self) -> &ContentId {
272 &self.request_content_id
273 }
274
275 pub fn status(&self) -> &Symbol {
277 &self.status
278 }
279
280 pub fn created_at_ms(&self) -> u64 {
282 self.created_at_ms
283 }
284
285 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#[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 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 pub fn id(&self) -> &str {
361 &self.id
362 }
363
364 pub fn run_id(&self) -> &str {
366 &self.run_id
367 }
368
369 pub fn sequence(&self) -> u64 {
371 self.sequence
372 }
373
374 pub fn kind(&self) -> &Symbol {
376 &self.kind
377 }
378
379 pub fn payload(&self) -> &Expr {
381 &self.payload
382 }
383
384 pub fn created_at_ms(&self) -> u64 {
386 self.created_at_ms
387 }
388
389 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#[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 pub fn new(response: GatewayResponse) -> Self {
441 Self { response }
442 }
443
444 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
466pub 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
476pub 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
517pub(crate) fn bytes_to_hex(bytes: &[u8]) -> String {
521 const HEX: &[u8; 16] = b"0123456789abcdef";
522 let mut output = String::with_capacity(bytes.len() * 2);
523 for byte in bytes {
524 output.push(char::from(HEX[usize::from(byte >> 4)]));
525 output.push(char::from(HEX[usize::from(byte & 0x0f)]));
526 }
527 output
528}