sim_codec_chat/providers/anthropic/
encode.rs1use serde_json::{Map, Value, json};
2use sim_kernel::{CodecId, Error, Expr, Result};
3
4use crate::{is_model_request_expr, validate_chat_transcript};
5
6use super::AnthropicRequestOptions;
7use super::common::{
8 codec_error, codec_eval_to_codec, expr_entries, flatten_expr, optional_expr, optional_string,
9 required_list, required_string, required_symbol, sim_expr_to_json,
10};
11
12pub fn encode_anthropic_request(expr: &Expr, options: &AnthropicRequestOptions) -> Result<Vec<u8>> {
14 if !is_model_request_expr(expr) {
15 return Err(Error::Eval(
16 "anthropic codec expects a model-request transcript".to_owned(),
17 ));
18 }
19 validate_chat_transcript(expr)?;
20 let entries = expr_entries(expr, "anthropic request transcript")?;
21 let (messages, system) = transcript_messages(entries)?;
22 let mut payload = Map::new();
23 payload.insert("model".to_owned(), Value::String(options.model.clone()));
24 payload.insert("max_tokens".to_owned(), json!(options.max_tokens));
25 payload.insert("stream".to_owned(), Value::Bool(options.stream));
26 if let Some(system) = system {
27 payload.insert("system".to_owned(), Value::String(system));
28 }
29 payload.insert("messages".to_owned(), Value::Array(messages));
30 if options.tools {
31 payload.insert("tools".to_owned(), Value::Array(tool_schemas(entries)?));
32 }
33 if let Some(tool_choice) = optional_expr(entries, "tool-choice") {
34 payload.insert("tool_choice".to_owned(), sim_expr_to_json(tool_choice));
35 }
36 serde_json::to_vec(&Value::Object(payload))
37 .map_err(|err| Error::Eval(format!("anthropic codec failed to encode request: {err}")))
38}
39
40pub fn encode_anthropic_response(expr: &Expr) -> Result<Vec<u8>> {
42 let value = response_json(expr)?;
43 serde_json::to_vec(&value)
44 .map_err(|err| Error::Eval(format!("anthropic codec failed to encode response: {err}")))
45}
46
47pub(super) fn encode_anthropic_response_for_codec(codec: CodecId, expr: &Expr) -> Result<String> {
48 validate_chat_transcript(expr).map_err(|err| codec_eval_to_codec(codec, err))?;
49 let value = response_json(expr).map_err(|err| codec_eval_to_codec(codec, err))?;
50 serde_json::to_string(&value).map_err(|err| codec_error(codec, err))
51}
52
53fn transcript_messages(entries: &[(Expr, Expr)]) -> Result<(Vec<Value>, Option<String>)> {
54 let mut messages = Vec::new();
55 let mut system = Vec::new();
56 for message in required_list(entries, "messages")? {
57 let Some(rendered) = message_to_json(message, &mut system)? else {
58 continue;
59 };
60 messages.push(rendered);
61 }
62 messages.push(json!({
63 "role": "user",
64 "content": [{
65 "type": "text",
66 "text": flatten_expr(
67 optional_expr(entries, "task")
68 .ok_or_else(|| Error::Eval("anthropic request missing task field".to_owned()))?
69 ),
70 }],
71 }));
72 let system = (!system.is_empty()).then(|| system.join("\n\n"));
73 Ok((messages, system))
74}
75
76fn message_to_json(expr: &Expr, system: &mut Vec<String>) -> Result<Option<Value>> {
77 let entries = expr_entries(expr, "anthropic message")?;
78 let role = required_symbol(entries, "role")?;
79 if role.name.as_ref() == "system" && role.namespace.is_none() {
80 system.push(system_message_text(entries)?);
81 return Ok(None);
82 }
83 Ok(Some(json!({
84 "role": role.name.as_ref(),
85 "content": required_list(entries, "content")?
86 .iter()
87 .map(content_part_to_json)
88 .collect::<Result<Vec<_>>>()?,
89 })))
90}
91
92fn system_message_text(entries: &[(Expr, Expr)]) -> Result<String> {
93 required_list(entries, "content")?
94 .iter()
95 .map(content_part_to_text)
96 .collect::<Result<Vec<_>>>()
97 .map(|parts| parts.join("\n"))
98}
99
100fn content_part_to_text(expr: &Expr) -> Result<String> {
101 let entries = expr_entries(expr, "anthropic content part")?;
102 match required_symbol(entries, "type")?.name.as_ref() {
103 "text" => Ok(required_string(entries, "text")?.to_owned()),
104 other => Err(Error::Eval(format!(
105 "anthropic system message does not support content part type {other}"
106 ))),
107 }
108}
109
110fn content_part_to_json(expr: &Expr) -> Result<Value> {
111 let entries = expr_entries(expr, "anthropic content part")?;
112 match required_symbol(entries, "type")?.name.as_ref() {
113 "text" => Ok(json!({
114 "type": "text",
115 "text": required_string(entries, "text")?,
116 })),
117 "tool-call" => Ok(json!({
118 "type": "tool_use",
119 "id": optional_string(entries, "id")?.unwrap_or_else(|| "toolu_sim".to_owned()),
120 "name": required_string(entries, "name")?,
121 "input": optional_expr(entries, "arguments")
122 .or_else(|| optional_expr(entries, "input"))
123 .map(sim_expr_to_json)
124 .unwrap_or_else(|| Value::Object(Map::new())),
125 })),
126 "tool-result" => {
127 let mut object = Map::new();
128 object.insert("type".to_owned(), Value::String("tool_result".to_owned()));
129 object.insert(
130 "tool_use_id".to_owned(),
131 Value::String(
132 optional_string_any(entries, &["tool-call-id", "id"])?
133 .unwrap_or_else(|| "toolu_sim".to_owned()),
134 ),
135 );
136 object.insert(
137 "content".to_owned(),
138 Value::String(
139 optional_expr(entries, "output")
140 .map(flatten_expr)
141 .unwrap_or_else(|| "".to_owned()),
142 ),
143 );
144 if let Some(Expr::Symbol(status)) = optional_expr(entries, "status")
145 && status.name.as_ref() != "ok"
146 {
147 object.insert("is_error".to_owned(), Value::Bool(true));
148 }
149 Ok(Value::Object(object))
150 }
151 other => Err(Error::Eval(format!(
152 "anthropic codec does not support content part type {other}"
153 ))),
154 }
155}
156
157fn tool_schemas(entries: &[(Expr, Expr)]) -> Result<Vec<Value>> {
158 let Some(tools) = optional_expr(entries, "tools") else {
159 return Ok(Vec::new());
160 };
161 let Expr::List(tools) = tools else {
162 return Err(Error::Eval(
163 "anthropic request tools field must be a list".to_owned(),
164 ));
165 };
166 tools.iter().map(tool_schema).collect()
167}
168
169fn tool_schema(expr: &Expr) -> Result<Value> {
170 let entries = expr_entries(expr, "anthropic tool schema")?;
171 let name = optional_string_any(entries, &["name", "openai-name", "symbol"])?
172 .ok_or_else(|| Error::Eval("anthropic tool schema missing name".to_owned()))?;
173 let description = optional_string(entries, "description")?;
174 let input_schema = optional_expr(entries, "input-schema")
175 .or_else(|| optional_expr(entries, "input_schema"))
176 .or_else(|| optional_expr(entries, "parameters"))
177 .map(sim_expr_to_json)
178 .unwrap_or_else(|| json!({"type":"object","properties":{}}));
179 let mut object = Map::new();
180 object.insert("name".to_owned(), Value::String(name));
181 if let Some(description) = description {
182 object.insert("description".to_owned(), Value::String(description));
183 }
184 object.insert("input_schema".to_owned(), input_schema);
185 Ok(Value::Object(object))
186}
187
188fn optional_string_any(entries: &[(Expr, Expr)], keys: &[&str]) -> Result<Option<String>> {
189 for key in keys {
190 if let Some(value) = optional_string(entries, key)? {
191 return Ok(Some(value));
192 }
193 }
194 Ok(None)
195}
196
197fn response_json(expr: &Expr) -> Result<Value> {
198 let entries = expr_entries(expr, "anthropic response transcript")?;
199 let model = required_string(entries, "model")?;
200 let stop_reason = required_symbol(entries, "stop-reason")?;
201 Ok(json!({
202 "id": "msg_sim",
203 "type": "message",
204 "role": "assistant",
205 "model": model,
206 "content": required_list(entries, "content")?
207 .iter()
208 .map(content_part_to_json)
209 .collect::<Result<Vec<_>>>()?,
210 "stop_reason": stop_reason.name.as_ref().replace('-', "_"),
211 "stop_sequence": Value::Null,
212 "usage": response_usage(entries)?,
213 }))
214}
215
216fn response_usage(entries: &[(Expr, Expr)]) -> Result<Value> {
217 let Some(usage) = optional_expr(entries, "usage") else {
218 return Ok(json!({"input_tokens":0,"output_tokens":0}));
219 };
220 let fields = expr_entries(usage, "anthropic usage field")?;
221 Ok(json!({
222 "input_tokens": optional_u64_field(fields, "input-tokens")?.unwrap_or(0),
223 "output_tokens": optional_u64_field(fields, "output-tokens")?.unwrap_or(0),
224 }))
225}
226
227fn optional_u64_field(entries: &[(Expr, Expr)], key: &str) -> Result<Option<u64>> {
228 let Some(value) = optional_expr(entries, key) else {
229 return Ok(None);
230 };
231 match value {
232 Expr::Number(number) => number
233 .canonical
234 .parse::<u64>()
235 .map(Some)
236 .map_err(|err| Error::Eval(format!("anthropic usage field {key} invalid: {err}"))),
237 Expr::String(text) => text
238 .parse::<u64>()
239 .map(Some)
240 .map_err(|err| Error::Eval(format!("anthropic usage field {key} invalid: {err}"))),
241 other => Err(Error::Eval(format!(
242 "anthropic usage field {key} must be a number, found {other:?}"
243 ))),
244 }
245}