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