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