sim_lib_openai_server/codec_openai/
encode.rs1use serde_json::{Value, json};
2use sim_codec_chat::{is_model_request_expr, validate_chat_transcript};
3use sim_codec_json::json_number_to_u64;
4use sim_kernel::{CodecId, Error, Expr, Result};
5
6use crate::codec_openai::codec_error;
7use crate::codec_openai::decode::map_field;
8use crate::codec_openai::shapes::OpenAiCodecOptions;
9
10pub fn encode_openai_request(expr: &Expr, options: &OpenAiCodecOptions) -> Result<Vec<u8>> {
13 if !is_model_request_expr(expr) {
14 return Err(Error::Eval(
15 "openai codec expects a model-request transcript".to_owned(),
16 ));
17 }
18 validate_chat_transcript(expr)?;
19 let mut payload = json!({
20 "model": options.model,
21 "stream": options.stream,
22 "messages": transcript_messages(expr)?,
23 "tools": if options.tools { Value::Array(Vec::new()) } else { Value::Null },
24 });
25 if options.stream
26 && let Some(object) = payload.as_object_mut()
27 {
28 object.insert("stream_options".to_owned(), json!({"include_usage": true}));
29 }
30 serde_json::to_vec(&payload)
31 .map_err(|err| Error::Eval(format!("openai codec failed to encode request: {err}")))
32}
33
34pub fn encode_openai_response(expr: &Expr) -> Result<Vec<u8>> {
36 let value = response_json(expr)?;
37 serde_json::to_vec(&value)
38 .map_err(|err| Error::Eval(format!("openai codec failed to encode response: {err}")))
39}
40
41pub fn encode_openai_responses_response(
44 expr: &Expr,
45 response_id: &str,
46 created_at_ms: u64,
47) -> Result<Vec<u8>> {
48 let value = responses_response_json(expr, response_id, created_at_ms)?;
49 serde_json::to_vec(&value).map_err(|err| {
50 Error::Eval(format!(
51 "openai codec failed to encode responses response: {err}"
52 ))
53 })
54}
55
56pub(crate) fn encode_openai_response_for_codec(codec: CodecId, expr: &Expr) -> Result<String> {
57 if !marker_is_true(expr, "model-response") {
58 return Err(codec_error(
59 codec,
60 "openai codec expects a model-response transcript",
61 ));
62 }
63 validate_chat_transcript(expr).map_err(|err| match err {
64 Error::Eval(message) => codec_error(codec, message),
65 other => other,
66 })?;
67 let value = response_json(expr).map_err(|err| match err {
68 Error::Eval(message) => codec_error(codec, message),
69 other => other,
70 })?;
71 serde_json::to_string(&value).map_err(|err| codec_error(codec, err))
72}
73
74fn response_json(expr: &Expr) -> Result<Value> {
75 let Expr::Map(entries) = expr else {
76 return Err(Error::Eval(
77 "openai codec expects response transcript as a map".to_owned(),
78 ));
79 };
80 let model = string_field(entries, "model")?;
81 let finish_reason = symbol_field(entries, "stop-reason")?;
82 Ok(json!({
83 "id": "chatcmpl-sim",
84 "object": "chat.completion",
85 "created": 0,
86 "model": model,
87 "choices": [{
88 "index": 0,
89 "message": {
90 "role": "assistant",
91 "content": response_text(entries)?,
92 },
93 "finish_reason": finish_reason,
94 }],
95 "usage": response_usage(entries)?,
96 }))
97}
98
99fn responses_response_json(expr: &Expr, response_id: &str, created_at_ms: u64) -> Result<Value> {
100 let Expr::Map(entries) = expr else {
101 return Err(Error::Eval(
102 "openai codec expects response transcript as a map".to_owned(),
103 ));
104 };
105 let model = string_field(entries, "model")?;
106 let output_text = response_text(entries)?;
107 Ok(json!({
108 "id": response_id,
109 "object": "response",
110 "created_at": created_at_ms / 1000,
111 "status": "completed",
112 "model": model,
113 "output": [{
114 "type": "message",
115 "status": "completed",
116 "role": "assistant",
117 "content": [{
118 "type": "output_text",
119 "text": output_text,
120 "annotations": [],
121 }],
122 }],
123 "output_text": output_text,
124 "usage": response_usage(entries)?,
125 }))
126}
127
128fn response_text(entries: &[(Expr, Expr)]) -> Result<String> {
129 list_field(map_field(entries, "content")?)?
130 .iter()
131 .map(text_content)
132 .collect::<Result<Vec<_>>>()
133 .map(|parts| parts.join(""))
134}
135
136fn response_usage(entries: &[(Expr, Expr)]) -> Result<Value> {
137 let Some(usage) = entries.iter().find_map(|(field, value)| match field {
138 Expr::Symbol(symbol) if symbol.name.as_ref() == "usage" => Some(value),
139 _ => None,
140 }) else {
141 return Ok(Value::Null);
142 };
143 let Expr::Map(fields) = usage else {
144 return Err(Error::Eval(
145 "openai codec usage field must be a map".to_owned(),
146 ));
147 };
148 let prompt = optional_u64_field(fields, "input-tokens")?;
149 let completion = optional_u64_field(fields, "output-tokens")?;
150 let total =
151 optional_u64_field(fields, "total-tokens")?.or_else(|| match (prompt, completion) {
152 (Some(left), Some(right)) => Some(left + right),
153 _ => None,
154 });
155 Ok(json!({
156 "prompt_tokens": prompt.unwrap_or(0),
157 "completion_tokens": completion.unwrap_or(0),
158 "total_tokens": total.unwrap_or(0),
159 }))
160}
161
162fn transcript_messages(expr: &Expr) -> Result<Vec<Value>> {
163 let Expr::Map(entries) = expr else {
164 return Err(Error::Eval(
165 "openai codec expects request transcript as a map".to_owned(),
166 ));
167 };
168 let mut messages = list_field(map_field(entries, "messages")?)?
169 .iter()
170 .map(message_to_json)
171 .collect::<Result<Vec<_>>>()?;
172 messages.push(json!({
173 "role": "user",
174 "content": [{
175 "type": "text",
176 "text": flatten_expr(map_field(entries, "task")?),
177 }],
178 }));
179 Ok(messages)
180}
181
182fn message_to_json(expr: &Expr) -> Result<Value> {
183 let Expr::Map(entries) = expr else {
184 return Err(Error::Eval("openai codec message must be a map".to_owned()));
185 };
186 Ok(json!({
187 "role": symbol_field(entries, "role")?,
188 "content": list_field(map_field(entries, "content")?)?
189 .iter()
190 .map(content_part_to_json)
191 .collect::<Result<Vec<_>>>()?,
192 }))
193}
194
195fn content_part_to_json(expr: &Expr) -> Result<Value> {
196 let Expr::Map(entries) = expr else {
197 return Err(Error::Eval(
198 "openai codec content part must be a map".to_owned(),
199 ));
200 };
201 match symbol_field(entries, "type")?.as_str() {
202 "text" => Ok(json!({
203 "type": "text",
204 "text": string_field(entries, "text")?,
205 })),
206 other => Err(Error::Eval(format!(
207 "openai codec does not support content part type {other}"
208 ))),
209 }
210}
211
212fn text_content(expr: &Expr) -> Result<String> {
213 let Expr::Map(entries) = expr else {
214 return Err(Error::Eval(
215 "openai codec content part must be a map".to_owned(),
216 ));
217 };
218 match symbol_field(entries, "type")?.as_str() {
219 "text" => string_field(entries, "text"),
220 other => Err(Error::Eval(format!(
221 "openai codec does not support content part type {other}"
222 ))),
223 }
224}
225
226fn optional_u64_field(entries: &[(Expr, Expr)], key: &str) -> Result<Option<u64>> {
227 let Some(value) = entries.iter().find_map(|(field, value)| match field {
228 Expr::Symbol(symbol) if symbol.name.as_ref() == key => Some(value),
229 _ => None,
230 }) else {
231 return Ok(None);
232 };
233 match value {
234 Expr::Number(number) => number
235 .canonical
236 .parse::<u64>()
237 .map(Some)
238 .map_err(|err| Error::Eval(format!("openai codec invalid {key}: {err}"))),
239 other => {
240 let json_number = match other {
241 Expr::String(text) => serde_json::from_str::<Value>(text).ok(),
242 _ => None,
243 };
244 json_number
245 .as_ref()
246 .and_then(json_number_to_u64)
247 .ok_or_else(|| Error::Eval(format!("openai codec field {key} must be a number")))
248 .map(Some)
249 }
250 }
251}
252
253fn marker_is_true(expr: &Expr, name: &str) -> bool {
254 let Expr::Map(entries) = expr else {
255 return false;
256 };
257 entries.iter().any(|(key, value)| {
258 matches!(key, Expr::Symbol(symbol) if symbol.name.as_ref() == name)
259 && matches!(value, Expr::Bool(true))
260 })
261}
262
263fn symbol_field(entries: &[(Expr, Expr)], key: &str) -> Result<String> {
264 match map_field(entries, key)? {
265 Expr::Symbol(symbol) => Ok(symbol.name.as_ref().to_owned()),
266 _ => Err(Error::Eval(format!(
267 "openai codec field {key} must be a symbol"
268 ))),
269 }
270}
271
272fn string_field(entries: &[(Expr, Expr)], key: &str) -> Result<String> {
273 match map_field(entries, key)? {
274 Expr::String(text) => Ok(text.clone()),
275 _ => Err(Error::Eval(format!(
276 "openai codec field {key} must be a string"
277 ))),
278 }
279}
280
281fn list_field(expr: &Expr) -> Result<&[Expr]> {
282 match expr {
283 Expr::List(items) => Ok(items),
284 _ => Err(Error::Eval("openai codec field must be a list".to_owned())),
285 }
286}
287
288fn flatten_expr(expr: &Expr) -> String {
289 match expr {
290 Expr::Nil => "nil".to_owned(),
291 Expr::Bool(flag) => flag.to_string(),
292 Expr::Number(number) => number.canonical.clone(),
293 Expr::Symbol(symbol) | Expr::Local(symbol) => symbol.to_string(),
294 Expr::String(text) => text.clone(),
295 Expr::Bytes(bytes) => format!("{bytes:?}"),
296 Expr::List(items) | Expr::Vector(items) | Expr::Set(items) | Expr::Block(items) => {
297 items.iter().map(flatten_expr).collect::<Vec<_>>().join(" ")
298 }
299 Expr::Map(entries) => entries
300 .iter()
301 .map(|(key, value)| format!("{} {}", flatten_expr(key), flatten_expr(value)))
302 .collect::<Vec<_>>()
303 .join(" "),
304 Expr::Call { operator, args } => std::iter::once(flatten_expr(operator))
305 .chain(args.iter().map(flatten_expr))
306 .collect::<Vec<_>>()
307 .join(" "),
308 Expr::Infix {
309 operator,
310 left,
311 right,
312 } => format!(
313 "{} {} {}",
314 flatten_expr(left),
315 operator,
316 flatten_expr(right)
317 ),
318 Expr::Prefix { operator, arg } => format!("{operator} {}", flatten_expr(arg)),
319 Expr::Postfix { operator, arg } => format!("{} {operator}", flatten_expr(arg)),
320 Expr::Quote { expr, .. } | Expr::Annotated { expr, .. } => flatten_expr(expr),
321 Expr::Extension { tag, payload } => format!("{tag} {}", flatten_expr(payload)),
322 }
323}