1use async_trait::async_trait;
7use reqwest::Client;
8use serde::Deserialize;
9use serde_json::Value;
10use std::time::Duration;
11
12use super::{Completion, LlmProvider, StreamSender, TokenUsage, ToolCall, ToolSpec};
13use crate::error::{Error, Result};
14use crate::message::{Message, Role};
15
16#[derive(Debug, Clone)]
18pub struct RetryPolicy {
19 pub max_retries: usize,
20 pub initial_backoff: Duration,
21 pub max_backoff: Duration,
22}
23
24impl Default for RetryPolicy {
25 fn default() -> Self {
26 Self {
27 max_retries: 2,
28 initial_backoff: Duration::from_secs(1),
29 max_backoff: Duration::from_secs(8),
30 }
31 }
32}
33
34impl RetryPolicy {
35 pub fn backoff_for(
37 &self,
38 attempt: usize,
39 status: Option<u16>,
40 is_network_error: bool,
41 ) -> Option<Duration> {
42 if attempt >= self.max_retries {
43 return None;
44 }
45
46 let is_transient = is_network_error || status.is_some_and(|s| (500..600).contains(&s));
47
48 if !is_transient {
49 return None;
50 }
51
52 let backoff = self.initial_backoff * 2u32.pow(attempt as u32);
53 Some(backoff.min(self.max_backoff))
54 }
55}
56
57#[derive(Debug, Clone)]
58pub struct AnthropicProvider {
59 base_url: String,
60 api_key: String,
61 model: String,
62 client: Client,
63 temperature: f64,
64 max_tokens: u32,
65 retry: RetryPolicy,
66}
67
68impl AnthropicProvider {
69 pub fn new(
70 base_url: impl Into<String>,
71 api_key: impl Into<String>,
72 model: impl Into<String>,
73 ) -> Self {
74 Self {
75 base_url: base_url.into().trim_end_matches('/').to_string(),
76 api_key: api_key.into(),
77 model: model.into(),
78 client: Client::builder()
79 .timeout(Duration::from_secs(180))
80 .build()
81 .expect("reqwest client build"),
82 temperature: 0.2,
83 max_tokens: 4096,
84 retry: RetryPolicy::default(),
85 }
86 }
87
88 fn make_err(&self, ctx: impl Into<String>) -> Error {
90 Error::Llm {
91 provider: self.model.clone(),
92 message: ctx.into(),
93 }
94 }
95
96 pub fn with_temperature(mut self, t: f64) -> Self {
97 self.temperature = t;
98 self
99 }
100
101 pub fn with_max_tokens(mut self, n: u32) -> Self {
102 self.max_tokens = n;
103 self
104 }
105
106 pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
107 self.retry = policy;
108 self
109 }
110}
111
112#[async_trait]
113impl LlmProvider for AnthropicProvider {
114 async fn complete(&self, messages: &[Message], tools: &[ToolSpec]) -> Result<Completion> {
115 let (system, messages) = extract_system_message(messages);
116
117 let messages = filter_leading_assistant(&messages);
119
120 let body = build_request(
121 &self.model,
122 self.temperature,
123 self.max_tokens,
124 system.as_deref(),
125 &messages,
126 tools,
127 );
128 let url = format!("{}/v1/messages", self.base_url);
129
130 let mut attempt = 0;
131 loop {
132 tracing::debug!(target: "recursive::llm", request = %body, "POST {}", url);
133 let result = self
134 .client
135 .post(&url)
136 .header("x-api-key", &self.api_key)
137 .header("anthropic-version", "2023-06-01")
138 .header("content-type", "application/json")
139 .json(&body)
140 .send()
141 .await;
142
143 match result {
144 Ok(resp) => {
145 let status = resp.status();
146 let is_network_error = false;
147
148 if status.is_success() {
149 let text = resp.text().await?;
150 let parsed: AnthropicResponse =
151 serde_json::from_str(&text).map_err(|e| {
152 self.make_err(format!(
153 "failed to parse response: {e}; body: {text}"
154 ))
155 })?;
156 return Ok(parse_completion(parsed));
157 }
158
159 let text = resp.text().await?;
161 tracing::debug!(target: "recursive::llm", body = %text, "error response");
162
163 if let Some(backoff) =
164 self.retry
165 .backoff_for(attempt, Some(status.as_u16()), is_network_error)
166 {
167 tracing::warn!(
168 target: "recursive::llm",
169 attempt,
170 backoff_ms = backoff.as_millis(),
171 status = status.as_u16(),
172 "transient HTTP error, retrying"
173 );
174 tokio::time::sleep(backoff).await;
175 attempt += 1;
176 continue;
177 }
178
179 return Err(self.make_err(format!("HTTP {}: {}", status, text)));
181 }
182 Err(e) => {
183 if let Some(backoff) = self.retry.backoff_for(attempt, None, true) {
185 tracing::warn!(
186 target: "recursive::llm",
187 attempt,
188 backoff_ms = backoff.as_millis(),
189 error = %e,
190 "network error, retrying"
191 );
192 tokio::time::sleep(backoff).await;
193 attempt += 1;
194 continue;
195 }
196
197 return Err(self.make_err(format!("request failed: {e}")));
198 }
199 }
200 }
201 }
202
203 async fn stream(
204 &self,
205 messages: &[Message],
206 tools: &[ToolSpec],
207 stream_tx: Option<StreamSender>,
208 ) -> Result<Completion> {
209 self.stream_inner(messages, tools, stream_tx).await
210 }
211}
212
213impl AnthropicProvider {
214 async fn stream_inner(
216 &self,
217 messages: &[Message],
218 tools: &[ToolSpec],
219 stream_tx: Option<StreamSender>,
220 ) -> Result<Completion> {
221 let (system, messages) = extract_system_message(messages);
222 let messages = filter_leading_assistant(&messages);
223
224 let mut body = build_request(
225 &self.model,
226 self.temperature,
227 self.max_tokens,
228 system.as_deref(),
229 &messages,
230 tools,
231 );
232 body["stream"] = Value::Bool(true);
233
234 let url = format!("{}/v1/messages", self.base_url);
235
236 let mut attempt = 0;
237 loop {
238 tracing::debug!(target: "recursive::llm", request = %body, "POST {} (stream)", url);
239 let result = self
240 .client
241 .post(&url)
242 .header("x-api-key", &self.api_key)
243 .header("anthropic-version", "2023-06-01")
244 .header("content-type", "application/json")
245 .json(&body)
246 .send()
247 .await;
248
249 match result {
250 Ok(resp) => {
251 let status = resp.status();
252 if status.is_success() {
253 return self.parse_sse_stream(resp, stream_tx.clone()).await;
254 }
255
256 let text = resp.text().await?;
258 tracing::debug!(target: "recursive::llm", body = %text, "error response (stream)");
259
260 if let Some(backoff) =
261 self.retry
262 .backoff_for(attempt, Some(status.as_u16()), false)
263 {
264 tracing::warn!(
265 target: "recursive::llm",
266 attempt,
267 backoff_ms = backoff.as_millis(),
268 status = status.as_u16(),
269 "transient HTTP error, retrying (stream)"
270 );
271 tokio::time::sleep(backoff).await;
272 attempt += 1;
273 continue;
274 }
275
276 return Err(self.make_err(format!("HTTP {}: {}", status, text)));
277 }
278 Err(e) => {
279 if let Some(backoff) = self.retry.backoff_for(attempt, None, true) {
280 tracing::warn!(
281 target: "recursive::llm",
282 attempt,
283 backoff_ms = backoff.as_millis(),
284 error = %e,
285 "network error, retrying (stream)"
286 );
287 tokio::time::sleep(backoff).await;
288 attempt += 1;
289 continue;
290 }
291
292 return Err(self.make_err(format!("request failed: {e}")));
293 }
294 }
295 }
296 }
297
298 async fn parse_sse_stream(
312 &self,
313 resp: reqwest::Response,
314 stream_tx: Option<StreamSender>,
315 ) -> Result<Completion> {
316 let mut content = String::new();
317 let mut tool_calls: Vec<StreamToolCall> = Vec::new();
318 let mut finish_reason: Option<String> = None;
319 let mut usage: Option<TokenUsage> = None;
320 let mut input_tokens: Option<u32> = None;
321 let mut output_tokens: Option<u32> = None;
322 let mut cache_creation: Option<u32> = None;
323 let mut cache_read: Option<u32> = None;
324
325 let reader = resp.text().await?;
327 let mut current_event: Option<String> = None;
328
329 for line in reader.lines() {
330 if let Some(event_name) = line.strip_prefix("event: ") {
331 current_event = Some(event_name.to_string());
332 continue;
333 }
334
335 if let Some(data) = line.strip_prefix("data: ") {
336 let event_type = current_event.as_deref().unwrap_or("unknown");
337
338 match event_type {
339 "message_start" => {
340 let parsed: Value = serde_json::from_str(data).map_err(|e| {
342 self.make_err(format!(
343 "SSE parse error (message_start): {e}; data: {data}"
344 ))
345 })?;
346 if let Some(msg) = parsed.get("message") {
347 if let Some(u) = msg.get("usage") {
348 input_tokens = u
349 .get("input_tokens")
350 .and_then(|v| v.as_u64())
351 .map(|v| v as u32);
352 cache_creation = u
353 .get("cache_creation_input_tokens")
354 .and_then(|v| v.as_u64())
355 .map(|v| v as u32);
356 cache_read = u
357 .get("cache_read_input_tokens")
358 .and_then(|v| v.as_u64())
359 .map(|v| v as u32);
360 }
361 }
362 }
363 "content_block_start" => {
364 let parsed: Value = serde_json::from_str(data).map_err(|e| {
365 self.make_err(format!(
366 "SSE parse error (content_block_start): {e}; data: {data}"
367 ))
368 })?;
369 if let Some(block) = parsed.get("content_block") {
370 let block_type =
371 block.get("type").and_then(|v| v.as_str()).unwrap_or("");
372 let index =
373 parsed.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
374 match block_type {
375 "tool_use" => {
376 let id = block
377 .get("id")
378 .and_then(|v| v.as_str())
379 .unwrap_or("")
380 .to_string();
381 let name = block
382 .get("name")
383 .and_then(|v| v.as_str())
384 .unwrap_or("")
385 .to_string();
386 while tool_calls.len() <= index {
388 tool_calls.push(StreamToolCall::default());
389 }
390 tool_calls[index].id = id;
391 tool_calls[index].name = name;
392 }
393 "text" => {
394 if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
396 if !text.is_empty() {
397 content.push_str(text);
398 if let Some(ref tx) = stream_tx {
399 let _ = tx.send(text.to_string());
400 }
401 }
402 }
403 }
404 _ => {}
405 }
406 }
407 }
408 "content_block_delta" => {
409 let parsed: Value = serde_json::from_str(data).map_err(|e| {
410 self.make_err(format!(
411 "SSE parse error (content_block_delta): {e}; data: {data}"
412 ))
413 })?;
414 if let Some(delta) = parsed.get("delta") {
415 let delta_type =
416 delta.get("type").and_then(|v| v.as_str()).unwrap_or("");
417 match delta_type {
418 "text_delta" => {
419 if let Some(text) = delta.get("text").and_then(|v| v.as_str()) {
420 if !text.is_empty() {
421 content.push_str(text);
422 if let Some(ref tx) = stream_tx {
423 let _ = tx.send(text.to_string());
424 }
425 }
426 }
427 }
428 "input_json_delta" => {
429 if let Some(partial) =
430 delta.get("partial_json").and_then(|v| v.as_str())
431 {
432 let index = parsed
433 .get("index")
434 .and_then(|v| v.as_u64())
435 .unwrap_or(0)
436 as usize;
437 while tool_calls.len() <= index {
438 tool_calls.push(StreamToolCall::default());
439 }
440 tool_calls[index].partial_json.push_str(partial);
441 }
442 }
443 _ => {}
444 }
445 }
446 }
447 "message_delta" => {
448 let parsed: Value = serde_json::from_str(data).map_err(|e| {
449 self.make_err(format!(
450 "SSE parse error (message_delta): {e}; data: {data}"
451 ))
452 })?;
453 if let Some(delta) = parsed.get("delta") {
454 if let Some(reason) = delta.get("stop_reason").and_then(|v| v.as_str())
455 {
456 finish_reason = Some(reason.to_string());
457 }
458 }
459 if let Some(u) = parsed.get("usage") {
460 output_tokens = u
461 .get("output_tokens")
462 .and_then(|v| v.as_u64())
463 .map(|v| v as u32);
464 }
465 }
466 "message_stop" => {
467 }
469 "ping" => {
470 }
472 _ => {
473 tracing::debug!(target: "recursive::llm", event = %event_type, data = %data, "unhandled SSE event");
474 }
475 }
476
477 current_event = None;
478 }
479 }
480
481 if input_tokens.is_some() || output_tokens.is_some() {
483 let prompt = input_tokens.unwrap_or(0);
484 let completion = output_tokens.unwrap_or(0);
485 usage = Some(TokenUsage {
486 prompt_tokens: prompt,
487 completion_tokens: completion,
488 total_tokens: prompt.saturating_add(completion),
489 cache_hit_tokens: cache_read.unwrap_or(0),
490 cache_miss_tokens: cache_creation.unwrap_or(0),
491 });
492 }
493
494 let final_tool_calls: Vec<ToolCall> = tool_calls
496 .into_iter()
497 .filter(|tc| !tc.id.is_empty())
498 .map(|tc| {
499 let args: Value = if tc.partial_json.trim().is_empty() {
500 Value::Object(Default::default())
501 } else {
502 serde_json::from_str(&tc.partial_json)
503 .unwrap_or_else(|_| Value::String(tc.partial_json.clone()))
504 };
505 ToolCall {
506 id: tc.id,
507 name: tc.name,
508 arguments: args,
509 }
510 })
511 .collect();
512
513 Ok(Completion {
514 content,
515 tool_calls: final_tool_calls,
516 finish_reason: finish_reason.map(|r| match r.as_str() {
517 "end_turn" => "stop".to_string(),
518 "max_tokens" => "length".to_string(),
519 "tool_use" => "tool_calls".to_string(),
520 other => other.to_string(),
521 }),
522 usage,
523 reasoning_content: None,
524 })
525 }
526}
527
528fn extract_system_message(messages: &[Message]) -> (Option<String>, Vec<Message>) {
530 if messages.first().is_some_and(|m| m.role == Role::System) {
531 let system = messages[0].content.clone();
532 let rest = messages[1..].to_vec();
533 (Some(system), rest)
534 } else {
535 (None, messages.to_vec())
536 }
537}
538
539fn filter_leading_assistant(messages: &[Message]) -> Vec<Message> {
542 let mut result = messages.to_vec();
543 while result
544 .first()
545 .is_some_and(|m| m.role == Role::Assistant && m.tool_calls.is_empty())
546 {
547 result.remove(0);
549 }
550 result
551}
552
553fn build_request(
554 model: &str,
555 temperature: f64,
556 max_tokens: u32,
557 system: Option<&str>,
558 messages: &[Message],
559 tools: &[ToolSpec],
560) -> Value {
561 let mut req = serde_json::json!({
562 "model": model,
563 "max_tokens": max_tokens,
564 "temperature": temperature,
565 });
566
567 if let Some(sys) = system {
568 req["system"] = Value::String(sys.to_string());
569 }
570
571 let msgs: Vec<Value> = messages.iter().map(serialize_message).collect();
572 req["messages"] = Value::Array(msgs);
573
574 if !tools.is_empty() {
575 let tools_json: Vec<Value> = tools
576 .iter()
577 .map(|t| {
578 serde_json::json!({
579 "name": t.name,
580 "description": t.description,
581 "input_schema": t.parameters,
582 })
583 })
584 .collect();
585 req["tools"] = Value::Array(tools_json);
586 }
587
588 req
589}
590
591fn serialize_message(m: &Message) -> Value {
592 let role = match m.role {
593 Role::System => "system",
594 Role::User => "user",
595 Role::Assistant => "assistant",
596 Role::Tool => "user", };
598
599 if !m.tool_calls.is_empty() {
601 let tool_uses: Vec<Value> = m
602 .tool_calls
603 .iter()
604 .map(|c| {
605 serde_json::json!({
606 "type": "tool_use",
607 "id": c.id,
608 "name": c.name,
609 "input": c.arguments,
610 })
611 })
612 .collect();
613 serde_json::json!({
614 "role": role,
615 "content": tool_uses,
616 })
617 } else if let Some(id) = &m.tool_call_id {
618 serde_json::json!({
620 "role": role,
621 "content": [{
622 "type": "tool_result",
623 "tool_use_id": id,
624 "content": m.content,
625 }],
626 })
627 } else {
628 serde_json::json!({
630 "role": role,
631 "content": m.content,
632 })
633 }
634}
635
636#[derive(Debug, Deserialize)]
637struct AnthropicResponse {
638 #[serde(default)]
639 content: Vec<ContentBlock>,
640 #[serde(default)]
641 usage: Option<AnthropicUsage>,
642 #[serde(default)]
643 stop_reason: Option<String>,
644}
645
646#[derive(Debug, Deserialize)]
647#[serde(tag = "type")]
648enum ContentBlock {
649 #[serde(rename = "text")]
650 Text {
651 #[serde(default)]
652 text: String,
653 },
654 #[serde(rename = "tool_use")]
655 ToolUse {
656 id: String,
657 name: String,
658 #[serde(default)]
659 input: Value,
660 },
661}
662
663#[derive(Debug, Deserialize)]
664struct AnthropicUsage {
665 #[serde(default)]
666 input_tokens: Option<u32>,
667 #[serde(default)]
668 output_tokens: Option<u32>,
669 #[serde(default)]
670 cache_creation_input_tokens: Option<u32>,
671 #[serde(default)]
672 cache_read_input_tokens: Option<u32>,
673}
674
675impl AnthropicUsage {
676 fn to_token_usage(&self) -> TokenUsage {
677 TokenUsage {
678 prompt_tokens: self.input_tokens.unwrap_or(0),
679 completion_tokens: self.output_tokens.unwrap_or(0),
680 total_tokens: self
681 .input_tokens
682 .unwrap_or(0)
683 .saturating_add(self.output_tokens.unwrap_or(0)),
684 cache_hit_tokens: self.cache_read_input_tokens.unwrap_or(0),
685 cache_miss_tokens: self.cache_creation_input_tokens.unwrap_or(0),
686 }
687 }
688}
689
690#[derive(Default)]
692struct StreamToolCall {
693 id: String,
694 name: String,
695 partial_json: String,
696}
697
698fn parse_completion(response: AnthropicResponse) -> Completion {
699 let mut content = String::new();
700 let mut tool_calls = Vec::new();
701
702 for block in response.content {
703 match block {
704 ContentBlock::Text { text } => {
705 content.push_str(&text);
706 }
707 ContentBlock::ToolUse { id, name, input } => {
708 tool_calls.push(ToolCall {
709 id,
710 name,
711 arguments: input,
712 });
713 }
714 }
715 }
716
717 let finish_reason = response.stop_reason.map(|r| match r.as_str() {
718 "end_turn" => "stop".to_string(),
719 "max_tokens" => "length".to_string(),
720 "tool_use" => "tool_calls".to_string(),
721 other => other.to_string(),
722 });
723
724 Completion {
725 content,
726 tool_calls,
727 finish_reason,
728 usage: response.usage.map(|u| u.to_token_usage()),
729 reasoning_content: None,
730 }
731}
732
733#[cfg(test)]
734mod tests {
735 use super::*;
736
737 #[test]
738 fn extract_system_message_when_present() {
739 let msgs = vec![
740 Message::system("You are helpful.".to_string()),
741 Message::user("Hello".to_string()),
742 ];
743 let (system, rest) = extract_system_message(&msgs);
744 assert_eq!(system, Some("You are helpful.".to_string()));
745 assert_eq!(rest.len(), 1);
746 assert_eq!(rest[0].role, Role::User);
747 }
748
749 #[test]
750 fn extract_system_message_when_not_present() {
751 let msgs = vec![Message::user("Hello".to_string())];
752 let (system, rest) = extract_system_message(&msgs);
753 assert_eq!(system, None);
754 assert_eq!(rest.len(), 1);
755 }
756
757 #[test]
758 fn filter_leading_assistant_removes_empty_assistant() {
759 let msgs = vec![
760 Message::assistant("Hi".to_string()),
761 Message::user("Hello".to_string()),
762 ];
763 let filtered = filter_leading_assistant(&msgs);
764 assert_eq!(filtered.len(), 1);
765 assert_eq!(filtered[0].role, Role::User);
766 }
767
768 #[test]
769 fn filter_leading_assistant_keeps_assistant_with_tool_calls() {
770 let msgs = vec![
771 Message::assistant_with_tool_calls(
772 "I'll use a tool".to_string(),
773 vec![ToolCall {
774 id: "call_1".to_string(),
775 name: "test".to_string(),
776 arguments: Value::Object(Default::default()),
777 }],
778 ),
779 Message::user("Hello".to_string()),
780 ];
781 let filtered = filter_leading_assistant(&msgs);
782 assert_eq!(filtered.len(), 2);
784 }
785
786 #[test]
787 fn parses_text_response() {
788 let raw = r#"{
789 "type": "message",
790 "content": [{"type": "text", "text": "Hello"}],
791 "stop_reason": "end_turn",
792 "usage": {"input_tokens": 10, "output_tokens": 5}
793 }"#;
794 let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
795 let c = parse_completion(parsed);
796 assert_eq!(c.content, "Hello");
797 assert!(c.tool_calls.is_empty());
798 assert_eq!(c.finish_reason, Some("stop".to_string()));
799 assert!(c.usage.is_some());
800 }
801
802 #[test]
803 fn parses_tool_use_response() {
804 let raw = r#"{
805 "type": "message",
806 "content": [
807 {"type": "tool_use", "id": "call_1", "name": "read_file", "input": {"path": "src/lib.rs"}}
808 ],
809 "stop_reason": "tool_use",
810 "usage": {"input_tokens": 100, "output_tokens": 50}
811 }"#;
812 let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
813 let c = parse_completion(parsed);
814 assert!(c.content.is_empty());
815 assert_eq!(c.tool_calls.len(), 1);
816 assert_eq!(c.tool_calls[0].id, "call_1");
817 assert_eq!(c.tool_calls[0].name, "read_file");
818 assert_eq!(c.tool_calls[0].arguments["path"], "src/lib.rs");
819 }
820
821 #[test]
822 fn parses_mixed_content_response() {
823 let raw = r#"{
824 "type": "message",
825 "content": [
826 {"type": "text", "text": "I'll read that file. "},
827 {"type": "tool_use", "id": "call_1", "name": "read_file", "input": {"path": "src/lib.rs"}}
828 ],
829 "stop_reason": "tool_use",
830 "usage": {"input_tokens": 50, "output_tokens": 25}
831 }"#;
832 let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
833 let c = parse_completion(parsed);
834 assert_eq!(c.content, "I'll read that file. ");
835 assert_eq!(c.tool_calls.len(), 1);
836 }
837
838 #[test]
839 fn parses_usage_correctly() {
840 let raw = r#"{
841 "type": "message",
842 "content": [{"type": "text", "text": "hi"}],
843 "stop_reason": "end_turn",
844 "usage": {
845 "input_tokens": 100,
846 "output_tokens": 50,
847 "cache_creation_input_tokens": 20,
848 "cache_read_input_tokens": 30
849 }
850 }"#;
851 let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
852 let c = parse_completion(parsed);
853 let u = c.usage.unwrap();
854 assert_eq!(u.prompt_tokens, 100);
855 assert_eq!(u.completion_tokens, 50);
856 assert_eq!(u.cache_hit_tokens, 30);
857 assert_eq!(u.cache_miss_tokens, 20);
858 }
859
860 #[test]
861 fn serializes_tool_result_message() {
862 let msg = Message::tool_result("call_123".to_string(), "result content".to_string());
863 let v = serialize_message(&msg);
864 assert_eq!(v["role"], "user");
865 let content = &v["content"][0];
866 assert_eq!(content["type"], "tool_result");
867 assert_eq!(content["tool_use_id"], "call_123");
868 assert_eq!(content["content"], "result content");
869 }
870
871 #[test]
872 fn serializes_assistant_with_tool_calls() {
873 let msg = Message::assistant_with_tool_calls(
874 "I'll use a tool".to_string(),
875 vec![ToolCall {
876 id: "abc".to_string(),
877 name: "write_file".to_string(),
878 arguments: serde_json::json!({"path": "a", "contents": "b"}),
879 }],
880 );
881 let v = serialize_message(&msg);
882 assert_eq!(v["role"], "assistant");
883 let content = &v["content"][0];
884 assert_eq!(content["type"], "tool_use");
885 assert_eq!(content["name"], "write_file");
886 }
887
888 #[test]
889 fn builds_request_with_system() {
890 let req = build_request(
891 "claude-3",
892 0.2,
893 4096,
894 Some("You are helpful."),
895 &[Message::user("Hi".to_string())],
896 &[],
897 );
898 assert_eq!(req["system"], "You are helpful.");
899 assert_eq!(req["model"], "claude-3");
900 }
901
902 #[test]
903 fn builds_request_without_system() {
904 let req = build_request(
905 "claude-3",
906 0.2,
907 4096,
908 None,
909 &[Message::user("Hi".to_string())],
910 &[],
911 );
912 assert!(req.get("system").is_none());
913 }
914
915 #[test]
916 fn builds_request_with_tools() {
917 let tools = vec![ToolSpec {
918 name: "test".to_string(),
919 description: "A test tool".to_string(),
920 parameters: serde_json::json!({"type": "object"}),
921 }];
922 let req = build_request(
923 "claude-3",
924 0.2,
925 4096,
926 None,
927 &[Message::user("Hi".to_string())],
928 &tools,
929 );
930 assert!(req.get("tools").is_some());
931 }
932
933 #[tokio::test]
934 async fn test_a_mock_server_returns_canned_response() {
935 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
937 let addr = listener.local_addr().unwrap();
938
939 let handle = std::thread::spawn(move || {
940 let (mut stream, _) = listener.accept().unwrap();
941 use std::io::{Read, Write};
942 let mut buf = [0u8; 4096];
943 let _ = stream.read(&mut buf);
944
945 let body = r#"{"type":"message","content":[{"type":"text","text":"Hello from Anthropic"}],"stop_reason":"end_turn","usage":{"input_tokens":10,"output_tokens":5}}"#;
946 let response = format!(
947 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
948 body.len(),
949 body
950 );
951 write!(stream, "{}", response).unwrap();
952 stream.flush().unwrap();
953 });
954
955 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
957
958 let provider =
959 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
960 let result = provider
961 .complete(&[Message::user("hi".to_string())], &[])
962 .await;
963
964 let _ = handle.join();
965
966 let completion = result.expect("should succeed");
967 assert!(completion.content.contains("Hello from Anthropic"));
968 assert!(completion.usage.is_some());
969 let u = completion.usage.unwrap();
970 assert_eq!(u.prompt_tokens, 10);
971 assert_eq!(u.completion_tokens, 5);
972 }
973
974 #[tokio::test]
975 async fn test_b_error_response_includes_model_name() {
976 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
978 let addr = listener.local_addr().unwrap();
979
980 let handle = std::thread::spawn(move || {
981 let (mut stream, _) = listener.accept().unwrap();
982 use std::io::{Read, Write};
983 let mut buf = [0u8; 4096];
984 let _ = stream.read(&mut buf);
985
986 let body = r#"{"error":{"type":"authentication_error","message":"Invalid API Key"}}"#;
987 let response = format!(
988 "HTTP/1.1 401 Unauthorized\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
989 body.len(),
990 body
991 );
992 write!(stream, "{}", response).unwrap();
993 stream.flush().unwrap();
994 });
995
996 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
997
998 let provider =
999 AnthropicProvider::new(format!("http://{addr}"), "sk-invalid", "claude-3-opus");
1000 let err = provider
1001 .complete(&[Message::user("hi".to_string())], &[])
1002 .await
1003 .unwrap_err();
1004
1005 let _ = handle.join();
1006
1007 let msg = err.to_string();
1008 assert!(
1009 msg.contains("claude-3-opus"),
1010 "error should contain model name: {msg}"
1011 );
1012 assert!(
1013 msg.contains("401"),
1014 "error should contain HTTP status: {msg}"
1015 );
1016 }
1017
1018 #[tokio::test]
1019 async fn test_c_tool_use_extracts_correct_tool_call() {
1020 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1022 let addr = listener.local_addr().unwrap();
1023
1024 let handle = std::thread::spawn(move || {
1025 let (mut stream, _) = listener.accept().unwrap();
1026 use std::io::{Read, Write};
1027 let mut buf = [0u8; 4096];
1028 let _ = stream.read(&mut buf);
1029
1030 let body = r#"{"type":"message","content":[{"type":"tool_use","id":"call_abc123","name":"read_file","input":{"path":"src/lib.rs"}}],"stop_reason":"tool_use","usage":{"input_tokens":50,"output_tokens":30}}"#;
1031 let response = format!(
1032 "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1033 body.len(),
1034 body
1035 );
1036 write!(stream, "{}", response).unwrap();
1037 stream.flush().unwrap();
1038 });
1039
1040 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1041
1042 let provider =
1043 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1044 let result = provider
1045 .complete(&[Message::user("read the file".to_string())], &[])
1046 .await;
1047
1048 let _ = handle.join();
1049
1050 let completion = result.expect("should succeed");
1051 assert_eq!(completion.tool_calls.len(), 1);
1052 let tc = &completion.tool_calls[0];
1053 assert_eq!(tc.id, "call_abc123");
1054 assert_eq!(tc.name, "read_file");
1055 assert_eq!(tc.arguments["path"], "src/lib.rs");
1056 }
1057
1058 #[tokio::test]
1059 async fn test_d_stream_request_includes_stream_true() {
1060 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1063 let addr = listener.local_addr().unwrap();
1064 let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
1065 let captured_clone = captured.clone();
1066
1067 std::thread::spawn(move || {
1068 let (mut stream, _) = listener.accept().unwrap();
1069 use std::io::{Read, Write};
1070 let mut buf = [0u8; 8192];
1071 let n = stream.read(&mut buf).unwrap();
1072 let request = String::from_utf8_lossy(&buf[..n]).to_string();
1073 if let Some(body_start) = request.find("\r\n\r\n") {
1074 let body = request[body_start + 4..].trim().to_string();
1075 *captured_clone.lock().unwrap() = body.clone();
1076 }
1077 let body = "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
1079 write!(
1080 stream,
1081 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1082 body.len(),
1083 body,
1084 ).unwrap();
1085 stream.flush().unwrap();
1086 });
1087
1088 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1089
1090 let provider =
1091 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1092 let _ = provider
1093 .stream(&[Message::user("hi".to_string())], &[], None)
1094 .await;
1095
1096 let body_str = captured.lock().unwrap().clone();
1097 let body: serde_json::Value = serde_json::from_str(&body_str).unwrap();
1098 assert_eq!(body["stream"], true);
1099 }
1100
1101 #[tokio::test]
1102 async fn test_e_stream_text_deltas_accumulate() {
1103 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1106 let addr = listener.local_addr().unwrap();
1107
1108 std::thread::spawn(move || {
1109 let (mut stream, _) = listener.accept().unwrap();
1110 use std::io::{Read, Write};
1111 let mut buf = [0u8; 4096];
1112 let _ = stream.read(&mut buf);
1113 let body = "\
1114event: message_start
1115data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}
1116
1117event: content_block_start
1118data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1119
1120event: content_block_delta
1121data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" \"}}
1122
1123event: content_block_delta
1124data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"World\"}}
1125
1126event: message_delta
1127data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1128
1129event: message_stop
1130data: {\"type\":\"message_stop\"}
1131
1132";
1133 write!(
1134 stream,
1135 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1136 body.len(),
1137 body,
1138 ).unwrap();
1139 stream.flush().unwrap();
1140 });
1141
1142 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1143
1144 let provider =
1145 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1146 let completion = provider
1147 .stream(&[Message::user("hi".to_string())], &[], None)
1148 .await
1149 .unwrap();
1150 assert_eq!(completion.content, "Hello World");
1151 assert_eq!(completion.finish_reason.as_deref(), Some("stop"));
1152 assert!(completion.usage.is_some());
1153 let u = completion.usage.unwrap();
1154 assert_eq!(u.prompt_tokens, 10);
1155 assert_eq!(u.completion_tokens, 5);
1156 }
1157
1158 #[tokio::test]
1159 async fn test_f_stream_tool_use_assembles_tool_calls() {
1160 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1163 let addr = listener.local_addr().unwrap();
1164
1165 std::thread::spawn(move || {
1166 let (mut stream, _) = listener.accept().unwrap();
1167 use std::io::{Read, Write};
1168 let mut buf = [0u8; 4096];
1169 let _ = stream.read(&mut buf);
1170 let body = "\
1171event: message_start
1172data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":50,\"output_tokens\":0}}}
1173
1174event: content_block_start
1175data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_abc123\",\"name\":\"read_file\",\"input\":{}}}
1176
1177event: content_block_delta
1178data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\":\\\"src/\"}}
1179
1180event: content_block_delta
1181data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"lib.rs\\\"}\"}}
1182
1183event: message_delta
1184data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":30}}
1185
1186event: message_stop
1187data: {\"type\":\"message_stop\"}
1188
1189";
1190 write!(
1191 stream,
1192 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1193 body.len(),
1194 body,
1195 ).unwrap();
1196 stream.flush().unwrap();
1197 });
1198
1199 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1200
1201 let provider =
1202 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1203 let completion = provider
1204 .stream(&[Message::user("read the file".to_string())], &[], None)
1205 .await
1206 .unwrap();
1207 assert_eq!(completion.tool_calls.len(), 1);
1208 let tc = &completion.tool_calls[0];
1209 assert_eq!(tc.id, "call_abc123");
1210 assert_eq!(tc.name, "read_file");
1211 assert_eq!(tc.arguments["path"], "src/lib.rs");
1212 assert_eq!(completion.finish_reason.as_deref(), Some("tool_calls"));
1213 }
1214
1215 #[tokio::test]
1216 async fn test_g_stream_tx_receives_text_chunks() {
1217 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1220 let addr = listener.local_addr().unwrap();
1221
1222 std::thread::spawn(move || {
1223 let (mut stream, _) = listener.accept().unwrap();
1224 use std::io::{Read, Write};
1225 let mut buf = [0u8; 4096];
1226 let _ = stream.read(&mut buf);
1227 let body = "\
1228event: message_start
1229data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":5,\"output_tokens\":0}}}
1230
1231event: content_block_start
1232data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"A\"}}
1233
1234event: content_block_delta
1235data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"B\"}}
1236
1237event: content_block_delta
1238data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"C\"}}
1239
1240event: message_delta
1241data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":3}}
1242
1243event: message_stop
1244data: {\"type\":\"message_stop\"}
1245
1246";
1247 write!(
1248 stream,
1249 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1250 body.len(),
1251 body,
1252 ).unwrap();
1253 stream.flush().unwrap();
1254 });
1255
1256 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1257
1258 let provider =
1259 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1260 let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1261 let completion = provider
1262 .stream(&[Message::user("hi".to_string())], &[], Some(tx))
1263 .await
1264 .unwrap();
1265 assert_eq!(completion.content, "ABC");
1266
1267 let mut deltas = Vec::new();
1269 while let Some(d) = rx.recv().await {
1270 deltas.push(d);
1271 if deltas.len() >= 3 {
1272 break;
1273 }
1274 }
1275 assert_eq!(deltas, vec!["A", "B", "C"]);
1276 }
1277
1278 #[tokio::test]
1279 async fn test_h_stream_with_end_turn_produces_stop_reason() {
1280 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1283 let addr = listener.local_addr().unwrap();
1284
1285 std::thread::spawn(move || {
1286 let (mut stream, _) = listener.accept().unwrap();
1287 use std::io::{Read, Write};
1288 let mut buf = [0u8; 4096];
1289 let _ = stream.read(&mut buf);
1290 let body = "\
1291event: message_start
1292data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}
1293
1294event: content_block_start
1295data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1296
1297event: message_delta
1298data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1299
1300event: message_stop
1301data: {\"type\":\"message_stop\"}
1302
1303";
1304 write!(
1305 stream,
1306 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1307 body.len(),
1308 body,
1309 ).unwrap();
1310 stream.flush().unwrap();
1311 });
1312
1313 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1314
1315 let provider =
1316 AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1317 let completion = provider
1318 .stream(&[Message::user("hi".to_string())], &[], None)
1319 .await
1320 .unwrap();
1321 assert_eq!(completion.content, "Hello");
1322 assert_eq!(completion.finish_reason.as_deref(), Some("stop"));
1323 assert!(completion.usage.is_some());
1324 }
1325
1326 #[test]
1327 fn policy_retries_5xx_with_exponential_backoff() {
1328 let policy = RetryPolicy::default();
1329 assert_eq!(
1330 policy.backoff_for(0, Some(503), false),
1331 Some(Duration::from_secs(1))
1332 );
1333 assert_eq!(
1334 policy.backoff_for(1, Some(500), false),
1335 Some(Duration::from_secs(2))
1336 );
1337 assert_eq!(policy.backoff_for(2, Some(500), false), None);
1338 }
1339
1340 #[test]
1341 fn policy_retries_network_errors() {
1342 let policy = RetryPolicy::default();
1343 assert_eq!(
1344 policy.backoff_for(0, None, true),
1345 Some(Duration::from_secs(1))
1346 );
1347 }
1348
1349 #[test]
1350 fn policy_does_not_retry_4xx() {
1351 let policy = RetryPolicy::default();
1352 assert_eq!(policy.backoff_for(0, Some(400), false), None);
1353 assert_eq!(policy.backoff_for(0, Some(401), false), None);
1354 assert_eq!(policy.backoff_for(0, Some(404), false), None);
1355 }
1356}