Skip to main content

sim_lib_openai_server/codec_openai/
encode.rs

1use 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
9/// Encodes a response transcript into OpenAI Responses API response JSON,
10/// stamping it with `response_id` and `created_at_ms`.
11pub 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}