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