sim_lib_openai_server/codec_openai/
decode.rs1use serde_json::{Map, Value};
2use sim_codec::Input;
3use sim_codec_chat::{model_response_expr, text_part, usage_record, validate_chat_transcript};
4use sim_codec_json::{JsonProjectionMode, json_number_to_u64, project_json_to_expr};
5use sim_kernel::{CodecId, Error, Expr, Result, Symbol};
6
7use crate::codec_openai::{codec_error, input_text};
8
9pub fn decode_openai_request(input: Input) -> Result<Expr> {
11 decode_openai_request_for_codec(CodecId(0), input)
12}
13
14pub(crate) fn decode_openai_request_for_codec(codec: CodecId, input: Input) -> Result<Expr> {
15 let source = input_text(codec, input)?;
16 let value = serde_json::from_str::<Value>(&source).map_err(|err| codec_error(codec, err))?;
17 let request = value
18 .as_object()
19 .ok_or_else(|| codec_error(codec, "openai request must be a json object"))?;
20 let model = string_member(codec, request, "model")?.to_owned();
21 let (task, messages) = request_task_and_messages(codec, request)?;
22 let mut entries = vec![
23 (Expr::Symbol(Symbol::new("model-request")), Expr::Bool(true)),
24 (Expr::Symbol(Symbol::new("task")), task),
25 (Expr::Symbol(Symbol::new("messages")), Expr::List(messages)),
26 (Expr::Symbol(Symbol::new("model")), Expr::String(model)),
27 ];
28 if let Some(stream) = request.get("stream").and_then(Value::as_bool) {
29 entries.push((Expr::Symbol(Symbol::new("stream")), Expr::Bool(stream)));
30 }
31 if let Some(privacy) = request.get("privacy").and_then(Value::as_str) {
32 entries.push((
33 Expr::Symbol(Symbol::new("privacy")),
34 Expr::String(privacy.to_owned()),
35 ));
36 }
37 if let Some(budget) = request.get("budget") {
38 entries.push((
39 Expr::Symbol(Symbol::new("budget")),
40 project_json_to_expr(budget, JsonProjectionMode::UntaggedInterop),
41 ));
42 }
43 if let Some(max_tokens) = request
44 .get("max_tokens")
45 .or_else(|| request.get("max-tokens"))
46 {
47 entries.push((
48 Expr::Symbol(Symbol::new("max-tokens")),
49 project_json_to_expr(max_tokens, JsonProjectionMode::UntaggedInterop),
50 ));
51 }
52 if let Some(tools) = request.get("tools") {
53 entries.push((
54 Expr::Symbol(Symbol::new("tools")),
55 project_json_to_expr(tools, JsonProjectionMode::UntaggedInterop),
56 ));
57 }
58 if let Some(choice) = request
59 .get("tool_choice")
60 .or_else(|| request.get("tool-choice"))
61 {
62 entries.push((
63 Expr::Symbol(Symbol::new("tool-choice")),
64 project_json_to_expr(choice, JsonProjectionMode::UntaggedInterop),
65 ));
66 }
67 let expr = Expr::Map(entries);
68 validate_chat_transcript(&expr)?;
69 Ok(expr)
70}
71
72fn request_task_and_messages(
73 codec: CodecId,
74 request: &Map<String, Value>,
75) -> Result<(Expr, Vec<Expr>)> {
76 if let Some(messages) = request.get("messages").and_then(Value::as_array) {
77 let (task_message, prior_messages) = messages
78 .split_last()
79 .ok_or_else(|| codec_error(codec, "openai request messages must not be empty"))?;
80 return Ok((
81 Expr::String(message_text(codec, task_message)?),
82 prior_messages
83 .iter()
84 .map(|message| message_expr(codec, message))
85 .collect::<Result<Vec<_>>>()?,
86 ));
87 }
88 let input = request
89 .get("input")
90 .ok_or_else(|| codec_error(codec, "openai request missing input"))?;
91 Ok((Expr::String(input_text_value(codec, input)?), Vec::new()))
92}
93
94fn input_text_value(codec: CodecId, value: &Value) -> Result<String> {
95 match value {
96 Value::String(text) => Ok(text.clone()),
97 _ => Err(codec_error(codec, "openai request input must be a string")),
98 }
99}
100
101pub fn decode_openai_response(
105 runner: Symbol,
106 model: &str,
107 body: &[u8],
108 include_raw: bool,
109) -> Result<Expr> {
110 let value: Value = serde_json::from_slice(body)
111 .map_err(|err| Error::Eval(format!("openai codec returned invalid json: {err}")))?;
112 let response = value
113 .as_object()
114 .ok_or_else(|| Error::Eval("openai response must be a json object".to_owned()))?;
115 let choice = response
116 .get("choices")
117 .and_then(Value::as_array)
118 .and_then(|choices| choices.first())
119 .and_then(Value::as_object)
120 .ok_or_else(|| Error::Eval("openai response missing choices[0]".to_owned()))?;
121 let message = choice
122 .get("message")
123 .and_then(Value::as_object)
124 .ok_or_else(|| Error::Eval("openai response missing choices[0].message".to_owned()))?;
125 let stop_reason = choice
126 .get("finish_reason")
127 .and_then(Value::as_str)
128 .unwrap_or("stop");
129 let mut entries = match model_response_expr(
130 runner,
131 model,
132 message_content(message)?,
133 Symbol::new(stop_reason),
134 ) {
135 Expr::Map(entries) => entries,
136 _ => unreachable!("model_response_expr always returns a map"),
137 };
138 if let Some(usage) = usage_expr(response)? {
139 entries.push((Expr::Symbol(Symbol::new("usage")), usage));
140 }
141 if include_raw {
142 entries.push((
143 Expr::Symbol(Symbol::new("raw-provider-response")),
144 project_json_to_expr(&value, JsonProjectionMode::UntaggedInterop),
145 ));
146 }
147 Ok(Expr::Map(entries))
148}
149
150fn message_expr(codec: CodecId, value: &Value) -> Result<Expr> {
151 let object = value
152 .as_object()
153 .ok_or_else(|| codec_error(codec, "openai message must be an object"))?;
154 let role = string_member(codec, object, "role")?;
155 Ok(Expr::Map(vec![
156 (
157 Expr::Symbol(Symbol::new("role")),
158 Expr::Symbol(Symbol::new(role)),
159 ),
160 (
161 Expr::Symbol(Symbol::new("content")),
162 Expr::List(content_parts(codec, object.get("content"))?),
163 ),
164 ]))
165}
166
167fn message_text(codec: CodecId, value: &Value) -> Result<String> {
168 let object = value
169 .as_object()
170 .ok_or_else(|| codec_error(codec, "openai task message must be an object"))?;
171 let role = string_member(codec, object, "role")?;
172 if role != "user" {
173 return Err(codec_error(
174 codec,
175 "openai request final message must have role user",
176 ));
177 }
178 let parts = content_parts(codec, object.get("content"))?;
179 parts
180 .iter()
181 .map(text_from_part)
182 .collect::<Result<Vec<_>>>()
183 .map(|items| items.join("\n"))
184}
185
186fn content_parts(codec: CodecId, content: Option<&Value>) -> Result<Vec<Expr>> {
187 match content {
188 Some(Value::String(text)) => Ok(vec![text_part(text)]),
189 Some(Value::Array(parts)) => parts
190 .iter()
191 .map(|part| request_part_from_json(codec, part))
192 .collect(),
193 Some(Value::Null) | None => Ok(Vec::new()),
194 _ => Err(codec_error(
195 codec,
196 "openai message content must be string, array, or null",
197 )),
198 }
199}
200
201fn request_part_from_json(codec: CodecId, value: &Value) -> Result<Expr> {
202 let object = value
203 .as_object()
204 .ok_or_else(|| codec_error(codec, "openai content part must be an object"))?;
205 let kind = object.get("type").and_then(Value::as_str).unwrap_or("text");
206 match kind {
207 "text" => Ok(text_part(
208 object
209 .get("text")
210 .and_then(Value::as_str)
211 .ok_or_else(|| codec_error(codec, "openai text content part missing text"))?,
212 )),
213 other => Err(codec_error(
214 codec,
215 format!("openai content part type {other} is not supported"),
216 )),
217 }
218}
219
220fn message_content(message: &Map<String, Value>) -> Result<Vec<Expr>> {
221 match message.get("content") {
222 Some(Value::String(text)) => Ok(vec![text_part(text)]),
223 Some(Value::Array(parts)) => parts
224 .iter()
225 .map(response_part_from_json)
226 .collect::<Result<Vec<_>>>(),
227 Some(Value::Null) | None => Ok(Vec::new()),
228 _ => Err(Error::Eval(
229 "openai response message content must be string, array, or null".to_owned(),
230 )),
231 }
232}
233
234fn response_part_from_json(value: &Value) -> Result<Expr> {
235 let object = value
236 .as_object()
237 .ok_or_else(|| Error::Eval("openai response content part must be an object".to_owned()))?;
238 let kind = object.get("type").and_then(Value::as_str).unwrap_or("text");
239 match kind {
240 "text" => Ok(text_part(
241 object
242 .get("text")
243 .and_then(Value::as_str)
244 .ok_or_else(|| Error::Eval("openai response text part missing text".to_owned()))?,
245 )),
246 other => Err(Error::Eval(format!(
247 "openai response content part type {other} is not supported"
248 ))),
249 }
250}
251
252fn usage_expr(response: &Map<String, Value>) -> Result<Option<Expr>> {
253 let Some(usage) = response.get("usage").and_then(Value::as_object) else {
254 return Ok(None);
255 };
256 let input = usage.get("prompt_tokens").and_then(json_number_to_u64);
257 let output = usage.get("completion_tokens").and_then(json_number_to_u64);
258 let total = usage.get("total_tokens").and_then(json_number_to_u64);
259 Ok(Some(Expr::Map(usage_record(input, output, total))))
261}
262
263fn text_from_part(part: &Expr) -> Result<String> {
264 let Expr::Map(entries) = part else {
265 return Err(Error::Eval(
266 "openai content part transcript must be a map".to_owned(),
267 ));
268 };
269 match map_field(entries, "text")? {
270 Expr::String(text) => Ok(text.clone()),
271 _ => Err(Error::Eval(
272 "openai text content part text field must be a string".to_owned(),
273 )),
274 }
275}
276
277fn string_member<'a>(
278 codec: CodecId,
279 object: &'a Map<String, Value>,
280 name: &str,
281) -> Result<&'a str> {
282 object
283 .get(name)
284 .and_then(Value::as_str)
285 .ok_or_else(|| codec_error(codec, format!("openai request missing string {name}")))
286}
287
288pub(crate) fn map_field<'a>(entries: &'a [(Expr, Expr)], key: &str) -> Result<&'a Expr> {
289 sim_value::access::entry_field(entries, key)
292 .ok_or_else(|| Error::Eval(format!("openai codec missing {key} field")))
293}