sim_codec_chat/providers/openai/
encode.rs1use serde_json::{Value, json};
2use sim_kernel::{CodecId, Error, Expr, Result};
3
4use crate::{is_model_request_expr, validate_chat_transcript};
5
6use super::OpenAiRequestOptions;
7use super::common::{
8 codec_error, codec_eval_to_codec, flatten_expr, list_field, map_field, marker_is_true,
9 optional_u64_field, string_field, symbol_field,
10};
11
12pub fn encode_openai_request(expr: &Expr, options: &OpenAiRequestOptions) -> Result<Vec<u8>> {
14 if !is_model_request_expr(expr) {
15 return Err(Error::Eval(
16 "openai codec expects a model-request transcript".to_owned(),
17 ));
18 }
19 validate_chat_transcript(expr)?;
20 let mut payload = json!({
21 "model": options.model,
22 "stream": options.stream,
23 "messages": transcript_messages(expr)?,
24 "tools": if options.tools { Value::Array(Vec::new()) } else { Value::Null },
25 });
26 if options.stream
27 && let Some(object) = payload.as_object_mut()
28 {
29 object.insert("stream_options".to_owned(), json!({"include_usage": true}));
30 }
31 serde_json::to_vec(&payload)
32 .map_err(|err| Error::Eval(format!("openai codec failed to encode request: {err}")))
33}
34
35pub fn encode_openai_response(expr: &Expr) -> Result<Vec<u8>> {
37 let value = response_json(expr)?;
38 serde_json::to_vec(&value)
39 .map_err(|err| Error::Eval(format!("openai codec failed to encode response: {err}")))
40}
41
42pub(in crate::providers) fn encode_openai_response_for_codec(
43 codec: CodecId,
44 expr: &Expr,
45) -> Result<String> {
46 if !marker_is_true(expr, "model-response") {
47 return Err(codec_error(
48 codec,
49 "openai codec expects a model-response transcript",
50 ));
51 }
52 validate_chat_transcript(expr).map_err(|err| codec_eval_to_codec(codec, err))?;
53 let value = response_json(expr).map_err(|err| codec_eval_to_codec(codec, err))?;
54 serde_json::to_string(&value).map_err(|err| codec_error(codec, err))
55}
56
57fn transcript_messages(expr: &Expr) -> Result<Vec<Value>> {
58 let Expr::Map(entries) = expr else {
59 return Err(Error::Eval(
60 "openai codec expects request transcript as a map".to_owned(),
61 ));
62 };
63 let mut messages = list_field(map_field(entries, "messages")?)?
64 .iter()
65 .map(message_to_json)
66 .collect::<Result<Vec<_>>>()?;
67 messages.push(json!({
68 "role": "user",
69 "content": [{
70 "type": "text",
71 "text": flatten_expr(map_field(entries, "task")?),
72 }],
73 }));
74 Ok(messages)
75}
76
77fn message_to_json(expr: &Expr) -> Result<Value> {
78 let Expr::Map(entries) = expr else {
79 return Err(Error::Eval("openai codec message must be a map".to_owned()));
80 };
81 Ok(json!({
82 "role": symbol_field(entries, "role")?,
83 "content": list_field(map_field(entries, "content")?)?
84 .iter()
85 .map(content_part_to_json)
86 .collect::<Result<Vec<_>>>()?,
87 }))
88}
89
90fn content_part_to_json(expr: &Expr) -> Result<Value> {
91 let Expr::Map(entries) = expr else {
92 return Err(Error::Eval(
93 "openai codec content part must be a map".to_owned(),
94 ));
95 };
96 match symbol_field(entries, "type")?.as_str() {
97 "text" => Ok(json!({
98 "type": "text",
99 "text": string_field(entries, "text")?,
100 })),
101 other => Err(Error::Eval(format!(
102 "openai codec does not support content part type {other}"
103 ))),
104 }
105}
106
107fn response_json(expr: &Expr) -> Result<Value> {
108 let Expr::Map(entries) = expr else {
109 return Err(Error::Eval(
110 "openai codec expects response transcript as a map".to_owned(),
111 ));
112 };
113 let model = string_field(entries, "model")?;
114 let finish_reason = symbol_field(entries, "stop-reason")?;
115 Ok(json!({
116 "id": "chatcmpl-sim",
117 "object": "chat.completion",
118 "created": 0,
119 "model": model,
120 "choices": [{
121 "index": 0,
122 "message": {
123 "role": "assistant",
124 "content": response_text(entries)?,
125 },
126 "finish_reason": finish_reason,
127 }],
128 "usage": response_usage(entries)?,
129 }))
130}
131
132fn response_text(entries: &[(Expr, Expr)]) -> Result<String> {
133 list_field(map_field(entries, "content")?)?
134 .iter()
135 .map(text_content)
136 .collect::<Result<Vec<_>>>()
137 .map(|parts| parts.join(""))
138}
139
140fn text_content(expr: &Expr) -> Result<String> {
141 let Expr::Map(entries) = expr else {
142 return Err(Error::Eval(
143 "openai codec content part must be a map".to_owned(),
144 ));
145 };
146 match symbol_field(entries, "type")?.as_str() {
147 "text" => string_field(entries, "text"),
148 other => Err(Error::Eval(format!(
149 "openai codec does not support content part type {other}"
150 ))),
151 }
152}
153
154fn response_usage(entries: &[(Expr, Expr)]) -> Result<Value> {
155 let Some(usage) = entries.iter().find_map(|(field, value)| match field {
156 Expr::Symbol(symbol) if symbol.name.as_ref() == "usage" => Some(value),
157 _ => None,
158 }) else {
159 return Ok(Value::Null);
160 };
161 let Expr::Map(fields) = usage else {
162 return Err(Error::Eval(
163 "openai codec usage field must be a map".to_owned(),
164 ));
165 };
166 let prompt = optional_u64_field(fields, "input-tokens")?;
167 let completion = optional_u64_field(fields, "output-tokens")?;
168 let total = optional_u64_field(fields, "total-tokens")?.or_else(|| {
169 prompt
170 .zip(completion)
171 .map(|(left, right)| left.saturating_add(right))
172 });
173 Ok(json!({
174 "prompt_tokens": prompt.unwrap_or(0),
175 "completion_tokens": completion.unwrap_or(0),
176 "total_tokens": total.unwrap_or(0),
177 }))
178}