1use crate::wire::intel::{Message, Request, Response, StopReason, ToolCall, Usage};
11use serde_json::{Map, Value, json};
12
13pub const DEFAULT_PATH: &str = "/v1/messages";
14const ANTHROPIC_VERSION: &str = "2023-06-01";
15
16pub fn build_request(req: &Request, token: Option<&str>) -> (Vec<u8>, Vec<(String, String)>) {
17 let system: String = req
19 .messages
20 .iter()
21 .filter_map(|m| match m {
22 Message::System(s) => Some(s.as_str()),
23 _ => None,
24 })
25 .collect::<Vec<_>>()
26 .join("\n\n");
27
28 let messages: Vec<Value> = req
29 .messages
30 .iter()
31 .filter_map(message_to_anthropic)
32 .collect();
33
34 let mut body = Map::new();
35 body.insert("model".into(), json!(req.model));
36 body.insert("max_tokens".into(), json!(req.max_tokens));
37 if !system.is_empty() {
38 body.insert("system".into(), json!(system));
39 }
40 if let Some(t) = req.temperature {
41 body.insert("temperature".into(), json!(t));
42 }
43 body.insert("messages".into(), json!(messages));
44 if !req.tools.is_empty() {
45 let tools: Vec<Value> = req
46 .tools
47 .iter()
48 .map(|t| json!({"name": t.name, "description": t.description, "input_schema": t.input_schema}))
49 .collect();
50 body.insert("tools".into(), json!(tools));
51 }
52
53 let bytes = serde_json::to_vec(&Value::Object(body)).unwrap_or_default();
54 let mut headers = vec![
55 ("content-type".to_string(), "application/json".to_string()),
56 (
57 "anthropic-version".to_string(),
58 ANTHROPIC_VERSION.to_string(),
59 ),
60 ];
61 if let Some(tok) = token {
62 headers.push(("x-api-key".to_string(), tok.to_string()));
63 }
64 (bytes, headers)
65}
66
67fn message_to_anthropic(m: &Message) -> Option<Value> {
68 match m {
69 Message::System(_) => None, Message::User(s) => Some(json!({"role": "user", "content": s})),
71 Message::Assistant { text, tool_calls } => {
72 let mut blocks: Vec<Value> = Vec::new();
73 if let Some(t) = text.as_deref().filter(|t| !t.is_empty()) {
74 blocks.push(json!({"type": "text", "text": t}));
75 }
76 for tc in tool_calls {
77 blocks.push(json!({"type": "tool_use", "id": tc.id, "name": tc.name, "input": tc.arguments}));
78 }
79 Some(json!({"role": "assistant", "content": blocks}))
80 }
81 Message::ToolResult {
82 id,
83 content,
84 is_error,
85 } => Some(json!({
86 "role": "user",
87 "content": [{
88 "type": "tool_result",
89 "tool_use_id": id,
90 "content": content,
91 "is_error": is_error,
92 }]
93 })),
94 }
95}
96
97pub fn parse_response(body: &[u8]) -> Result<Response, String> {
98 let v: Value =
99 serde_json::from_slice(body).map_err(|e| format!("intel: bad JSON response: {e}"))?;
100
101 if v.get("type").and_then(Value::as_str) == Some("error") {
102 let msg = v
103 .get("error")
104 .and_then(|e| e.get("message"))
105 .and_then(Value::as_str)
106 .unwrap_or("unknown");
107 return Err(format!("intel: provider error: {msg}"));
108 }
109
110 let mut text_parts: Vec<String> = Vec::new();
111 let mut tool_calls = Vec::new();
112 if let Some(blocks) = v.get("content").and_then(Value::as_array) {
113 for b in blocks {
114 match b.get("type").and_then(Value::as_str) {
115 Some("text") => {
116 if let Some(t) = b.get("text").and_then(Value::as_str) {
117 text_parts.push(t.to_string());
118 }
119 }
120 Some("tool_use") => {
121 tool_calls.push(ToolCall {
122 id: b
123 .get("id")
124 .and_then(Value::as_str)
125 .unwrap_or("")
126 .to_string(),
127 name: b
128 .get("name")
129 .and_then(Value::as_str)
130 .unwrap_or("")
131 .to_string(),
132 arguments: b.get("input").cloned().unwrap_or(Value::Null),
133 });
134 }
135 _ => {}
136 }
137 }
138 }
139
140 let stop_reason = match v.get("stop_reason").and_then(Value::as_str) {
141 Some("end_turn") | Some("stop_sequence") => StopReason::EndTurn,
142 Some("tool_use") => StopReason::ToolUse,
143 Some("max_tokens") => StopReason::MaxTokens,
144 _ => StopReason::Other,
145 };
146
147 let usage = v.get("usage").map(|u| Usage {
148 input_tokens: u.get("input_tokens").and_then(Value::as_u64).unwrap_or(0),
149 output_tokens: u.get("output_tokens").and_then(Value::as_u64).unwrap_or(0),
150 });
151
152 let text = if text_parts.is_empty() {
153 None
154 } else {
155 Some(text_parts.join(""))
156 };
157 Ok(Response {
158 text,
159 tool_calls,
160 stop_reason,
161 usage: usage.unwrap_or_default(),
162 })
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168 use crate::wire::intel::ToolDef;
169
170 #[test]
171 fn build_hoists_system_and_headers() {
172 let req = Request {
173 model: "claude-x".into(),
174 messages: vec![Message::system("be terse"), Message::user("hi")],
175 tools: vec![ToolDef {
176 name: "t".into(),
177 description: "d".into(),
178 input_schema: json!({}),
179 }],
180 max_tokens: 100,
181 temperature: None,
182 };
183 let (body, headers) = build_request(&req, Some("sk-ant"));
184 let v: Value = serde_json::from_slice(&body).unwrap();
185 assert_eq!(v["system"], "be terse");
186 assert_eq!(v["messages"][0]["role"], "user");
187 assert_eq!(v["tools"][0]["name"], "t");
188 assert!(
189 headers
190 .iter()
191 .any(|(k, val)| k == "x-api-key" && val == "sk-ant")
192 );
193 assert!(headers.iter().any(|(k, _)| k == "anthropic-version"));
194 }
195
196 #[test]
197 fn parse_text_and_tool_use() {
198 let body = br#"{"content":[{"type":"text","text":"hi"},{"type":"tool_use","id":"tu_1","name":"read","input":{"p":1}}],"stop_reason":"tool_use","usage":{"input_tokens":5,"output_tokens":7}}"#;
199 let r = parse_response(body).unwrap();
200 assert_eq!(r.text.as_deref(), Some("hi"));
201 assert_eq!(r.tool_calls[0].name, "read");
202 assert_eq!(r.tool_calls[0].arguments["p"], 1);
203 assert_eq!(r.stop_reason, StopReason::ToolUse);
204 assert_eq!(r.usage.total(), 12);
205 }
206}