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