1use crate::ai::adapter::{AiError, ProviderAdapter, ProviderErrorBody, ProviderRequest};
31use crate::ai::types::{
32 ChatMessage, ChatRequest, ChatResponse, ChatRole, Choice, ContentPart, FinishReason,
33 StreamDelta, StreamEvent, ToolCall, ToolCallDelta, Usage,
34};
35use crate::config::ai::AiProviderKind;
36use serde_json::{json, Map, Value};
37
38pub const DEFAULT_MAX_TOKENS: u64 = 4096;
42
43pub struct AnthropicAdapter;
45
46impl ProviderAdapter for AnthropicAdapter {
47 fn kind(&self) -> AiProviderKind {
48 AiProviderKind::Anthropic
49 }
50
51 fn build_request(
52 &self,
53 req: &ChatRequest,
54 provider_model: &str,
55 ) -> Result<ProviderRequest, AiError> {
56 let mut body = Map::new();
57 body.insert("model".into(), json!(provider_model));
58 let system: Vec<String> = req
60 .messages
61 .iter()
62 .filter(|m| m.role == ChatRole::System)
63 .map(|m| m.text_content())
64 .filter(|s| !s.is_empty())
65 .collect();
66 if !system.is_empty() {
67 body.insert("system".into(), json!(system.join("\n\n")));
68 }
69 let mut messages: Vec<Value> = Vec::new();
70 for m in &req.messages {
71 match m.role {
72 ChatRole::System => continue,
73 ChatRole::User | ChatRole::Assistant => messages.push(json!({
74 "role": if m.role == ChatRole::User { "user" } else { "assistant" },
75 "content": message_content_blocks(m),
76 })),
77 ChatRole::Tool => {
78 let Some(tool_call_id) = m.tool_call_id.as_deref() else {
86 return Err(AiError::InvalidRequest(
87 "a tool message carries no tool_call_id (the id that \
88 names the call it answers)"
89 .to_string(),
90 ));
91 };
92 let text = m.text_content();
93 messages.push(json!({
94 "role": "user",
95 "content": [{
96 "type": "tool_result",
97 "tool_use_id": tool_call_id,
98 "content": if text.is_empty() { Value::Null } else { json!(text) },
99 }],
100 }));
101 }
102 }
103 }
104 if messages.is_empty() {
105 return Err(AiError::InvalidRequest(
106 "the conversation has no non-system messages".to_string(),
107 ));
108 }
109 let messages = merge_consecutive_turns(messages);
112 body.insert("messages".into(), Value::Array(messages));
113 body.insert(
114 "max_tokens".into(),
115 json!(req.max_tokens.unwrap_or(DEFAULT_MAX_TOKENS)),
116 );
117 if !req.tools.is_empty() {
118 let tools: Vec<Value> = req
119 .tools
120 .iter()
121 .map(|t| {
122 let mut o = Map::new();
123 o.insert("name".into(), json!(t.name));
124 if let Some(d) = &t.description {
125 o.insert("description".into(), json!(d));
126 }
127 o.insert(
128 "input_schema".into(),
129 t.parameters
130 .clone()
131 .unwrap_or_else(|| json!({"type": "object"})),
132 );
133 Value::Object(o)
134 })
135 .collect();
136 body.insert("tools".into(), Value::Array(tools));
137 if let Some(choice) = &req.tool_choice {
138 match choice {
139 crate::ai::types::ToolChoice::Auto => {
140 body.insert("tool_choice".into(), json!({"type": "auto"}));
141 }
142 crate::ai::types::ToolChoice::None => {
143 body.remove("tools");
148 }
149 crate::ai::types::ToolChoice::Any => {
150 body.insert("tool_choice".into(), json!({"type": "any"}));
151 }
152 crate::ai::types::ToolChoice::Tool(name) => {
153 body.insert("tool_choice".into(), json!({"type": "tool", "name": name}));
154 }
155 }
156 }
157 }
158 if let Some(t) = req.temperature {
159 body.insert("temperature".into(), json!(t));
160 }
161 if let Some(p) = req.top_p {
162 body.insert("top_p".into(), json!(p));
163 }
164 if let Some(stop) = &req.stop {
165 body.insert("stop_sequences".into(), json!(stop));
166 }
167 if req.stream {
168 body.insert("stream".into(), json!(true));
169 }
170 if let Some(rf) = &req.response_format {
176 apply_anthropic_response_format(&mut body, rf);
177 }
178 Ok(ProviderRequest {
179 method: http::Method::POST,
180 path: "/v1/messages".to_string(),
181 headers: vec![
182 (http::header::CONTENT_TYPE, "application/json".to_string()),
183 (
186 http::HeaderName::from_static("anthropic-version"),
187 "2023-06-01".to_string(),
188 ),
189 ],
190 body: Value::Object(body),
191 })
192 }
193
194 fn parse_response(&self, body: &Value) -> Result<ChatResponse, AiError> {
195 let obj = body.as_object().ok_or_else(|| {
196 AiError::Translation("anthropic response is not a JSON object".to_string())
197 })?;
198 let content = obj
199 .get("content")
200 .and_then(Value::as_array)
201 .ok_or_else(|| AiError::Translation("anthropic response has no content".to_string()))?;
202 let mut text = String::new();
203 let mut tool_calls = Vec::new();
204 let mut image_parts = Vec::new();
205 for block in content {
206 match block.get("type").and_then(Value::as_str) {
207 Some("text") => text.push_str(
208 block
209 .get("text")
210 .and_then(Value::as_str)
211 .unwrap_or_default(),
212 ),
213 Some("tool_use") => tool_calls.push(ToolCall {
214 id: block
215 .get("id")
216 .and_then(Value::as_str)
217 .unwrap_or_default()
218 .to_string(),
219 name: block
220 .get("name")
221 .and_then(Value::as_str)
222 .unwrap_or_default()
223 .to_string(),
224 arguments: block
225 .get("input")
226 .cloned()
227 .unwrap_or(Value::Null)
228 .to_string(),
229 }),
230 Some("image") => {
231 let source = block.get("source").cloned().unwrap_or(Value::Null);
232 image_parts.push(ContentPart::Image {
233 url: None,
234 media_type: source
238 .get("media_type")
239 .and_then(Value::as_str)
240 .map(String::from)
241 .or_else(|| {
242 source
243 .get("type")
244 .and_then(Value::as_str)
245 .map(|t| format!("image/{t}"))
246 }),
247 data_b64: source.get("data").and_then(Value::as_str).map(String::from),
248 });
249 }
250 _ => {}
251 }
252 }
253 let mut message_content: Vec<ContentPart> = vec![ContentPart::Text { text }];
254 message_content.extend(image_parts);
255 let usage = obj.get("usage").map(|u| Usage {
256 prompt_tokens: u.get("input_tokens").and_then(Value::as_u64),
257 completion_tokens: u.get("output_tokens").and_then(Value::as_u64),
258 total_tokens: match (
259 u.get("input_tokens").and_then(Value::as_u64),
260 u.get("output_tokens").and_then(Value::as_u64),
261 ) {
262 (Some(i), Some(o)) => Some(i + o),
263 (i, o) => i.or(o),
264 },
265 cached_tokens: u.get("cache_read_input_tokens").and_then(Value::as_u64),
266 });
267 let finish_reason = match obj.get("stop_reason").and_then(Value::as_str) {
268 Some("end_turn") | Some("stop_sequence") | None => FinishReason::Stop,
269 Some("max_tokens") => FinishReason::Length,
270 Some("tool_use") => FinishReason::ToolCalls,
271 Some("refusal") => FinishReason::ContentFilter,
272 Some(other) => FinishReason::Other(other.to_string()),
273 };
274 Ok(ChatResponse {
275 id: obj.get("id").and_then(Value::as_str).map(String::from),
276 model: obj.get("model").and_then(Value::as_str).map(String::from),
277 choices: vec![Choice {
278 index: 0,
279 message: ChatMessage {
280 role: ChatRole::Assistant,
281 content: message_content,
282 name: None,
283 tool_calls,
284 tool_call_id: None,
285 },
286 finish_reason,
287 }],
288 usage,
289 })
290 }
291
292 fn parse_error(&self, body: &Value) -> ProviderErrorBody {
293 let e = body
295 .get("error")
296 .and_then(|e| e.as_object())
297 .or_else(|| body.as_object());
298 ProviderErrorBody {
299 message: e
300 .and_then(|e| e.get("message"))
301 .and_then(Value::as_str)
302 .unwrap_or("provider returned an error")
303 .to_string(),
304 error_type: e
305 .and_then(|e| e.get("type"))
306 .and_then(Value::as_str)
307 .map(String::from),
308 code: None,
309 }
310 }
311
312 fn parse_stream_event(&self, data: &Value) -> Result<Vec<StreamEvent>, AiError> {
313 let obj = data.as_object().ok_or_else(|| {
314 AiError::Translation("anthropic stream event is not a JSON object".to_string())
315 })?;
316 let kind = obj.get("type").and_then(Value::as_str).unwrap_or("");
317 let mut out = Vec::new();
318 match kind {
319 "message_start" => {
320 if let Some(u) = obj.get("message").and_then(|m| m.get("usage")) {
324 out.push(StreamEvent::Usage(Usage {
325 prompt_tokens: u.get("input_tokens").and_then(Value::as_u64),
326 completion_tokens: None,
327 total_tokens: None,
328 cached_tokens: u.get("cache_read_input_tokens").and_then(Value::as_u64),
329 }));
330 }
331 out.push(StreamEvent::Delta(StreamDelta {
333 index: 0,
334 role: Some(ChatRole::Assistant),
335 content: None,
336 tool_calls: Vec::new(),
337 finish_reason: None,
338 }));
339 }
340 "content_block_start" => {
341 let index = obj.get("index").and_then(Value::as_u64).unwrap_or(0);
342 if let Some(block) = obj.get("content_block") {
343 if block.get("type").and_then(Value::as_str) == Some("tool_use") {
344 out.push(StreamEvent::Delta(StreamDelta {
345 index: 0,
346 role: None,
347 content: None,
348 tool_calls: vec![ToolCallDelta {
349 index,
350 id: block.get("id").and_then(Value::as_str).map(String::from),
351 name: block.get("name").and_then(Value::as_str).map(String::from),
352 arguments: String::new(),
353 }],
354 finish_reason: None,
355 }));
356 }
357 }
358 }
359 "content_block_delta" => {
360 let index = obj.get("index").and_then(Value::as_u64).unwrap_or(0);
361 let delta = obj.get("delta").cloned().unwrap_or(Value::Null);
362 match delta.get("type").and_then(Value::as_str) {
363 Some("text_delta") => out.push(StreamEvent::Delta(StreamDelta {
364 index: 0,
365 role: None,
366 content: delta.get("text").and_then(Value::as_str).map(String::from),
367 tool_calls: Vec::new(),
368 finish_reason: None,
369 })),
370 Some("input_json_delta") => out.push(StreamEvent::Delta(StreamDelta {
371 index: 0,
372 role: None,
373 content: None,
374 tool_calls: vec![ToolCallDelta {
375 index,
376 id: None,
377 name: None,
378 arguments: delta
379 .get("partial_json")
380 .and_then(Value::as_str)
381 .unwrap_or_default()
382 .to_string(),
383 }],
384 finish_reason: None,
385 })),
386 _ => {}
387 }
388 }
389 "message_delta" => {
390 let delta = obj.get("delta").cloned().unwrap_or(Value::Null);
391 let finish = match delta.get("stop_reason").and_then(Value::as_str) {
392 Some("end_turn") | Some("stop_sequence") => FinishReason::Stop,
393 Some("max_tokens") => FinishReason::Length,
394 Some("tool_use") => FinishReason::ToolCalls,
395 Some("refusal") => FinishReason::ContentFilter,
396 other => other
397 .map(|s| FinishReason::Other(s.to_string()))
398 .unwrap_or(FinishReason::Stop),
399 };
400 out.push(StreamEvent::Delta(StreamDelta {
401 index: 0,
402 role: None,
403 content: None,
404 tool_calls: Vec::new(),
405 finish_reason: Some(finish),
406 }));
407 if let Some(u) = obj.get("usage") {
408 out.push(StreamEvent::Usage(Usage {
409 prompt_tokens: None,
410 completion_tokens: u.get("output_tokens").and_then(Value::as_u64),
411 total_tokens: None,
412 cached_tokens: None,
413 }));
414 }
415 }
416 "message_stop" => out.push(StreamEvent::Done),
417 _ => {}
418 }
419 Ok(out)
420 }
421}
422
423fn message_content_blocks(m: &ChatMessage) -> Value {
428 let mut blocks: Vec<Value> = Vec::new();
429 let mut text = String::new();
430 for part in &m.content {
431 match part {
432 ContentPart::Text { text: t } => text.push_str(t),
433 ContentPart::Image {
434 data_b64,
435 media_type,
436 url,
437 } => {
438 let decomposed = url.as_deref().and_then(crate::ai::types::split_data_uri);
444 let data = data_b64
445 .clone()
446 .or_else(|| decomposed.as_ref().map(|(_, data)| data.clone()));
447 if let Some(data) = data {
448 let mime = media_type
449 .clone()
450 .or_else(|| decomposed.as_ref().map(|(mime, _)| mime.clone()));
451 blocks.push(json!({
452 "type": "image",
453 "source": {
454 "type": "base64",
455 "media_type": mime.unwrap_or_else(|| "image/png".into()),
456 "data": data,
457 }
458 }));
459 }
460 }
461 }
462 }
463 if !text.is_empty() {
464 blocks.insert(0, json!({"type": "text", "text": text}));
465 }
466 if m.role == ChatRole::Assistant {
467 for tc in &m.tool_calls {
468 let input: Value = tc.arguments.trim().parse().unwrap_or_else(|_| json!({}));
469 blocks.push(json!({
470 "type": "tool_use",
471 "id": tc.id,
472 "name": tc.name,
473 "input": input,
474 }));
475 }
476 }
477 Value::Array(blocks)
478}
479
480fn merge_consecutive_turns(messages: Vec<Value>) -> Vec<Value> {
483 let mut merged: Vec<Value> = Vec::with_capacity(messages.len());
484 for m in messages {
485 let role = m
486 .get("role")
487 .and_then(Value::as_str)
488 .unwrap_or("")
489 .to_string();
490 let content = m.get("content").cloned().unwrap_or(Value::Null);
491 if let Some(last) = merged.last_mut() {
492 if last.get("role").and_then(Value::as_str) == Some(role.as_str()) {
493 let existing = last.get("content").and_then(Value::as_array).cloned();
494 let mut combined = existing.unwrap_or_default();
495 match content {
496 Value::Array(arr) => combined.extend(arr),
497 other => combined.push(other),
498 }
499 if let Some(obj) = last.as_object_mut() {
500 obj.insert("content".into(), Value::Array(combined));
501 }
502 continue;
503 }
504 }
505 merged.push(m);
506 }
507 merged
508}
509
510fn apply_anthropic_response_format(
517 body: &mut serde_json::Map<String, Value>,
518 rf: &crate::ai::types::ResponseFormat,
519) {
520 use crate::ai::types::ResponseFormat;
521 match rf {
522 ResponseFormat::Text => { }
523 ResponseFormat::JsonObject => {
524 let tool = serde_json::json!({
526 "name": "structured_output",
527 "description": "Output the result as a JSON object.",
528 "input_schema": {
529 "type": "object",
530 "properties": {},
531 "additionalProperties": true
532 }
533 });
534 let tools = body
535 .entry("tools".to_string())
536 .or_insert_with(|| Value::Array(Vec::new()));
537 if let Some(arr) = tools.as_array_mut() {
538 arr.push(tool);
539 }
540 body.insert("tool_choice".into(), serde_json::json!({"type": "any"}));
541 }
542 ResponseFormat::JsonSchema { name, schema } => {
543 let tool = serde_json::json!({
544 "name": name,
545 "description": "Output the result conforming to the provided JSON schema.",
546 "input_schema": schema
547 });
548 let tools = body
549 .entry("tools".to_string())
550 .or_insert_with(|| Value::Array(Vec::new()));
551 if let Some(arr) = tools.as_array_mut() {
552 arr.push(tool);
553 }
554 body.insert("tool_choice".into(), serde_json::json!({"type": "any"}));
555 }
556 }
557}