Skip to main content

recursive/llm/
anthropic.rs

1//! Anthropic Messages API adapter.
2//!
3//! Targets the `/v1/messages` endpoint that Anthropic and compatible
4//! providers (MiniMax, DeepSeek) speak.
5
6use 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/// Retry policy for transient failures (network timeouts, 5xx errors).
17#[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    /// Decide whether the caller should wait and try again.
36    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    /// Build an `Error::Llm` with the model name prefixed.
89    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        // Anthropic requires messages to not start with assistant role
118        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                    // Non-2xx response: check if it's transient (5xx)
160                    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                    // Non-transient (4xx or other)
180                    return Err(self.make_err(format!("HTTP {}: {}", status, text)));
181                }
182                Err(e) => {
183                    // Network error
184                    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    /// Internal streaming implementation.
215    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                    // Non-2xx: read body and check retry
257                    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    /// Parse an Anthropic SSE stream from a successful HTTP response.
299    ///
300    /// Anthropic SSE format:
301    ///   event: message_start\n
302    ///   data: {"type":"message_start","message":{...}}\n\n
303    ///   event: content_block_start\n
304    ///   data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":"Hello"}}\n\n
305    ///   event: content_block_delta\n
306    ///   data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" World"}}\n\n
307    ///   event: message_delta\n
308    ///   data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}\n\n
309    ///   event: message_stop\n
310    ///   data: {"type":"message_stop"}\n\n
311    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        // Read the full response body as text and parse line by line
326        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                        // Extract input tokens from the message
341                        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                                    // Ensure the vec is large enough
387                                    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                                    // Initial text content (if any)
395                                    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                        // End of stream - nothing to extract
468                    }
469                    "ping" => {
470                        // Anthropic sends periodic pings to keep the connection alive
471                    }
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        // Build final TokenUsage from accumulated fields
482        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        // Convert streamed tool calls to final ToolCall objects
495        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
528/// Extract system message if present, return (system_content, remaining_messages).
529fn 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
539/// Filter out any leading assistant messages that would cause Anthropic to reject the request.
540/// Anthropic requires the first message to be user, or system+user.
541fn 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        // Remove leading assistant message with no tool calls
548        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", // Anthropic uses "user" for tool results
597    };
598
599    // If this message has tool calls, format as tool_use blocks
600    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        // Tool result message - format as tool_result content block
619        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        // Regular text message
629        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/// Accumulator for tool calls being built from SSE stream events.
691#[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        // Should keep the assistant message because it has tool_calls
783        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        // Spawn a mock server that returns a valid Anthropic response
936        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        // Give the server a moment to start
956        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        // Spawn a mock server that returns 401
977        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        // Spawn a mock server that returns a tool_use response
1021        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        // Spawn a mock server that captures the request body and returns
1061        // a valid SSE response. Assert the request body contains "stream": true.
1062        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            // Return a valid SSE stream (just message_stop to end quickly)
1078            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        // Spawn a mock server that sends text deltas via SSE.
1104        // Assert the returned Completion's content is the concatenation.
1105        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        // Spawn a mock server that sends tool_use blocks via SSE.
1161        // Assert the returned Completion has the correct tool_calls.
1162        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        // Spawn a mock server that sends text deltas via SSE.
1218        // Assert the stream_tx channel receives incremental chunks.
1219        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        // Should have received 3 deltas
1268        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        // Spawn a mock server that sends a complete SSE stream with
1281        // end_turn stop_reason. Assert the Completion has finish_reason "stop".
1282        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}