sim_lib_openai_server/codec_openai/
encode.rs1use serde_json::{Value, json};
2pub use sim_codec_chat::{encode_openai_request, encode_openai_response};
3use sim_codec_json::json_number_to_u64;
4use sim_kernel::{Error, Expr, Result};
5use sim_value::access::{entry_field, entry_required_str_any, entry_required_sym_any};
6
7use crate::objects::canonical_json_bytes;
8
9pub fn encode_openai_responses_response(
12 expr: &Expr,
13 response_id: &str,
14 created_at_ms: u64,
15) -> Result<Vec<u8>> {
16 let value = responses_response_json(expr, response_id, created_at_ms)?;
17 Ok(canonical_json_bytes(value))
18}
19
20fn responses_response_json(expr: &Expr, response_id: &str, created_at_ms: u64) -> Result<Value> {
21 let Expr::Map(entries) = expr else {
22 return Err(Error::Eval(
23 "openai codec expects response transcript as a map".to_owned(),
24 ));
25 };
26 let model = string_field(entries, "model")?;
27 let output_text = response_text(entries)?;
28 Ok(json!({
29 "id": response_id,
30 "object": "response",
31 "created_at": created_at_ms / 1000,
32 "status": "completed",
33 "model": model,
34 "output": [{
35 "type": "message",
36 "status": "completed",
37 "role": "assistant",
38 "content": [{
39 "type": "output_text",
40 "text": output_text,
41 "annotations": [],
42 }],
43 }],
44 "output_text": output_text,
45 "usage": response_usage(entries)?,
46 }))
47}
48
49fn response_text(entries: &[(Expr, Expr)]) -> Result<String> {
50 list_field(map_field(entries, "content")?)?
51 .iter()
52 .map(text_content)
53 .collect::<Result<Vec<_>>>()
54 .map(|parts| parts.join(""))
55}
56
57fn response_usage(entries: &[(Expr, Expr)]) -> Result<Value> {
58 let Some(usage) = entries.iter().find_map(|(field, value)| match field {
59 Expr::Symbol(symbol) if symbol.name.as_ref() == "usage" => Some(value),
60 _ => None,
61 }) else {
62 return Ok(Value::Null);
63 };
64 let Expr::Map(fields) = usage else {
65 return Err(Error::Eval(
66 "openai codec usage field must be a map".to_owned(),
67 ));
68 };
69 let prompt = optional_u64_field(fields, "input-tokens")?;
70 let completion = optional_u64_field(fields, "output-tokens")?;
71 let total = optional_u64_field(fields, "total-tokens")?.or_else(|| {
72 prompt
73 .zip(completion)
74 .map(|(left, right)| left.saturating_add(right))
75 });
76 Ok(json!({
77 "prompt_tokens": prompt.unwrap_or(0),
78 "completion_tokens": completion.unwrap_or(0),
79 "total_tokens": total.unwrap_or(0),
80 }))
81}
82
83fn text_content(expr: &Expr) -> Result<String> {
84 let Expr::Map(entries) = expr else {
85 return Err(Error::Eval(
86 "openai codec content part must be a map".to_owned(),
87 ));
88 };
89 match symbol_field(entries, "type")?.as_str() {
90 "text" => string_field(entries, "text"),
91 other => Err(Error::Eval(format!(
92 "openai codec does not support content part type {other}"
93 ))),
94 }
95}
96
97fn optional_u64_field(entries: &[(Expr, Expr)], key: &str) -> Result<Option<u64>> {
98 let Some(value) = entries.iter().find_map(|(field, value)| match field {
99 Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(value),
100 _ => None,
101 }) else {
102 return Ok(None);
103 };
104 match value {
105 Expr::Number(number) => number
106 .canonical
107 .parse::<u64>()
108 .map(Some)
109 .map_err(|err| Error::Eval(format!("openai codec invalid {key}: {err}"))),
110 other => {
111 let json_number = match other {
112 Expr::String(text) => serde_json::from_str::<Value>(text).ok(),
113 _ => None,
114 };
115 json_number
116 .as_ref()
117 .and_then(json_number_to_u64)
118 .ok_or_else(|| Error::Eval(format!("openai codec field {key} must be a number")))
119 .map(Some)
120 }
121 }
122}
123
124fn symbol_field(entries: &[(Expr, Expr)], key: &str) -> Result<String> {
125 entry_required_sym_any(entries, key, "openai codec symbol field")
126 .map(|symbol| symbol.name.as_ref().to_owned())
127}
128
129fn string_field(entries: &[(Expr, Expr)], key: &str) -> Result<String> {
130 entry_required_str_any(entries, key, "openai codec string field").map(str::to_owned)
131}
132
133fn list_field(expr: &Expr) -> Result<&[Expr]> {
134 match expr {
135 Expr::List(items) => Ok(items),
136 _ => Err(Error::Eval("openai codec field must be a list".to_owned())),
137 }
138}
139
140fn map_field<'a>(entries: &'a [(Expr, Expr)], key: &str) -> Result<&'a Expr> {
141 entry_field(entries, key)
142 .ok_or_else(|| Error::Eval(format!("openai codec missing {key} field")))
143}