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 std::sync::Arc;
7use std::time::Duration;
8
9use async_trait::async_trait;
10use reqwest::Client;
11use serde::Deserialize;
12use serde_json::{json, Value};
13
14use super::search::{KeywordSearchEngine, SpecWithHint, ToolSearchEngine};
15use super::{Completion, LlmProvider, RetryPolicy, StreamSender, TokenUsage, ToolCall, ToolSpec};
16
17/// The name of the `ToolSearchTool` we register as a regular tool
18/// in the eager list. The model calls it like any other function
19/// tool; we recognize it by name and short-circuit the response
20/// with `tool_reference` content blocks.
21pub(crate) const TOOL_SEARCH_TOOL_NAME: &str = "ToolSearchTool";
22
23/// Hard cap on the number of search round-trips per
24/// `complete_with_search` call. Bounds the worst case where the
25/// model keeps calling `ToolSearchTool` with different queries.
26const MAX_SEARCH_ROUNDS: usize = 3;
27use crate::error::{Error, Result};
28use crate::message::{Message, Role};
29
30#[derive(Debug, Clone)]
31pub struct AnthropicProvider {
32    base_url: String,
33    api_key: String,
34    model: String,
35    client: Client,
36    temperature: f64,
37    max_tokens: u32,
38    retry: RetryPolicy,
39    /// Algorithm used to resolve a `ToolSearchTool` query into a
40    /// list of deferred tool names. Always non-null (defaults to
41    /// `KeywordSearchEngine`).
42    search_engine: Arc<dyn ToolSearchEngine>,
43}
44
45impl AnthropicProvider {
46    pub fn new(
47        base_url: impl Into<String>,
48        api_key: impl Into<String>,
49        model: impl Into<String>,
50    ) -> Self {
51        Self {
52            base_url: base_url.into().trim_end_matches('/').to_string(),
53            api_key: api_key.into(),
54            model: model.into(),
55            client: Client::builder()
56                .timeout(Duration::from_secs(180))
57                .build()
58                .expect("reqwest client build"),
59            temperature: 0.2,
60            max_tokens: 4096,
61            retry: RetryPolicy::default(),
62            search_engine: Arc::new(KeywordSearchEngine::new()),
63        }
64    }
65
66    /// Build an `Error::Llm` with the model name prefixed.
67    fn make_err(&self, ctx: impl Into<String>) -> Error {
68        Error::Llm {
69            provider: self.model.clone(),
70            message: ctx.into(),
71        }
72    }
73
74    pub fn with_temperature(mut self, t: f64) -> Self {
75        self.temperature = t;
76        self
77    }
78
79    pub fn with_max_tokens(mut self, n: u32) -> Self {
80        self.max_tokens = n;
81        self
82    }
83
84    pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
85        self.retry = policy;
86        self
87    }
88
89    /// Replace the default `KeywordSearchEngine` with a custom
90    /// implementation. Useful for tests that want to assert on
91    /// which tools the search returns.
92    pub fn with_search_engine(mut self, engine: Arc<dyn ToolSearchEngine>) -> Self {
93        self.search_engine = engine;
94        self
95    }
96
97    /// The `ToolSearchTool` spec — registered as a regular tool in
98    /// the eager list. The model calls it like any other function
99    /// tool; we recognize the call by `name == "ToolSearchTool"`.
100    fn tool_search_spec() -> (ToolSpec, Option<String>) {
101        let description = "Fetches full schema definitions for deferred tools so they can be \
102            called. Until fetched, only the name is known — there is no parameter schema, so \
103            the tool cannot be invoked. Use `select:<tool_name>` for direct selection, or \
104            keywords to search."
105            .to_string();
106        let search_hint = Some("find search discover discover tools lookup".to_string());
107        let spec = ToolSpec {
108            name: TOOL_SEARCH_TOOL_NAME.to_string(),
109            description,
110            parameters: json!({
111                "type": "object",
112                "properties": {
113                    "query": {
114                        "type": "string",
115                        "description": "Query to find deferred tools. Use \"select:<tool_name>\" \
116                            for direct selection, or keywords to search."
117                    },
118                    "max_results": {
119                        "type": "integer",
120                        "description": "Maximum number of results to return (default: 5)",
121                        "default": 5
122                    }
123                },
124                "required": ["query"]
125            }),
126        };
127        (spec, search_hint)
128    }
129
130    /// The core search-aware loop. Each iteration:
131    /// 1. Build a request with eager tools (no `defer_loading`)
132    ///    plus deferred tools (each marked `defer_loading: true`),
133    ///    plus `ToolSearchTool` (always eager).
134    /// 2. Send and parse the response.
135    /// 3. If the model called `ToolSearchTool`, resolve the
136    ///    query against the deferred candidates, append a
137    ///    `tool_result` with `tool_reference` content blocks, and
138    ///    recurse.
139    /// 4. Otherwise return the response.
140    ///
141    /// `pending_tool_result` is `Some((tool_use_id, names))` on
142    /// iterations after the first — it instructs the body builder
143    /// to replace the last (marker) message with a proper
144    /// `tool_result` content block whose `content` is an array of
145    /// `tool_reference` items.
146    async fn run_search_aware_loop(
147        &self,
148        messages: &[Message],
149        eager_tools: &[SpecWithHint],
150        deferred_tools: &[SpecWithHint],
151        pending_tool_result: Option<(&str, &[String])>,
152        round: usize,
153    ) -> Result<Completion> {
154        // Build the full eager list: caller's eager + ToolSearchTool.
155        let mut all_eager: Vec<SpecWithHint> = Vec::with_capacity(eager_tools.len() + 1);
156        all_eager.push(Self::tool_search_spec());
157        all_eager.extend_from_slice(eager_tools);
158
159        let (system, msgs) = extract_system_message(messages);
160        let msgs = filter_leading_assistant(&msgs);
161
162        let body = build_request_with_partition(
163            &self.model,
164            self.temperature,
165            self.max_tokens,
166            system.as_deref(),
167            &msgs,
168            &all_eager,
169            deferred_tools,
170            pending_tool_result,
171        );
172        let url = format!("{}/v1/messages", self.base_url);
173
174        let text = self.post_with_retry(&url, &body).await?;
175        let parsed: AnthropicResponse = serde_json::from_str(&text)
176            .map_err(|e| self.make_err(format!("failed to parse response: {e}; body: {text}")))?;
177        let completion = parse_completion(parsed);
178
179        // If the model called ToolSearchTool, resolve and recurse.
180        if let Some(search_call) = completion
181            .tool_calls
182            .iter()
183            .find(|c| c.name == TOOL_SEARCH_TOOL_NAME)
184        {
185            if round >= MAX_SEARCH_ROUNDS {
186                // Bail out: too many search rounds. Return what
187                // we have so the kernel can decide.
188                tracing::warn!(
189                    target: "recursive::llm",
190                    round,
191                    max = MAX_SEARCH_ROUNDS,
192                    "ToolSearchTool: hit MAX_SEARCH_ROUNDS, returning current completion"
193                );
194                return Ok(completion);
195            }
196            let query = search_call
197                .arguments
198                .get("query")
199                .and_then(|v| v.as_str())
200                .unwrap_or("");
201            let max_results = search_call
202                .arguments
203                .get("max_results")
204                .and_then(|v| v.as_u64())
205                .map(|n| n as usize);
206            let names = self.search_engine.resolve(query, deferred_tools);
207            let names: Vec<String> = match max_results {
208                Some(n) => names.into_iter().take(n).collect(),
209                None => names,
210            };
211
212            // Build the augmented transcript for the next request:
213            //   [...prev, assistant_with_call, user_marker]
214            //
215            // The user_marker is replaced with a proper
216            // `tool_result` block in the request body by
217            // `build_request_with_partition` (via
218            // `pending_tool_result`). We can't put the
219            // `tool_reference` array into `Message::content` as a
220            // string because the Anthropic API requires the
221            // `tool_result.content` field to be either a string
222            // (text result) or an array of content blocks — and
223            // a stringified JSON is parsed as text, not as
224            // `tool_reference` items.
225            let mut next_messages: Vec<Message> = msgs.clone();
226            let assistant_with_call = Message {
227                role: Role::Assistant,
228                content: completion.content.clone(),
229                tool_calls: completion.tool_calls.clone(),
230                tool_call_id: None,
231                reasoning_content: completion.reasoning_content.clone(),
232            };
233            next_messages.push(assistant_with_call);
234            next_messages.push(Message {
235                role: Role::User,
236                content: String::new(),
237                tool_calls: Vec::new(),
238                tool_call_id: Some(search_call.id.clone()),
239                reasoning_content: None,
240            });
241
242            return Box::pin(self.run_search_aware_loop(
243                &next_messages,
244                eager_tools,
245                deferred_tools,
246                Some((&search_call.id, &names)),
247                round + 1,
248            ))
249            .await;
250        }
251
252        Ok(completion)
253    }
254
255    /// POST `body` to `url` with the standard retry policy. Returns
256    /// the response body as a string. Used by both
257    /// `complete_with_search` (via `run_search_aware_loop`) and
258    /// `stream_inner`.
259    async fn post_with_retry(&self, url: &str, body: &Value) -> Result<String> {
260        let mut attempt = 0;
261        loop {
262            tracing::debug!(target: "recursive::llm", request = %body, "POST {}", url);
263            let result = self
264                .client
265                .post(url)
266                .header("x-api-key", &self.api_key)
267                .header("anthropic-version", "2023-06-01")
268                .header("content-type", "application/json")
269                .json(body)
270                .send()
271                .await;
272            match result {
273                Ok(resp) => {
274                    let status = resp.status();
275                    if status.is_success() {
276                        return resp.text().await.map_err(Error::from);
277                    }
278                    let text = resp.text().await?;
279                    if let Some(backoff) =
280                        self.retry
281                            .backoff_for(attempt, Some(status.as_u16()), false)
282                    {
283                        tracing::warn!(
284                            target: "recursive::llm",
285                            attempt,
286                            backoff_ms = backoff.as_millis(),
287                            status = status.as_u16(),
288                            "transient HTTP error, retrying"
289                        );
290                        tokio::time::sleep(backoff).await;
291                        attempt += 1;
292                        continue;
293                    }
294                    return Err(self.make_err(format!("HTTP {}: {}", status, text)));
295                }
296                Err(e) => {
297                    if let Some(backoff) = self.retry.backoff_for(attempt, None, true) {
298                        tracing::warn!(
299                            target: "recursive::llm",
300                            attempt,
301                            backoff_ms = backoff.as_millis(),
302                            error = %e,
303                            "network error, retrying"
304                        );
305                        tokio::time::sleep(backoff).await;
306                        attempt += 1;
307                        continue;
308                    }
309                    return Err(self.make_err(format!("request failed: {e}")));
310                }
311            }
312        }
313    }
314}
315
316#[async_trait]
317impl LlmProvider for AnthropicProvider {
318    async fn complete(&self, messages: &[Message], tools: &[ToolSpec]) -> Result<Completion> {
319        let (system, messages) = extract_system_message(messages);
320        let messages = filter_leading_assistant(&messages);
321        let body = build_request(
322            &self.model,
323            self.temperature,
324            self.max_tokens,
325            system.as_deref(),
326            &messages,
327            tools,
328        );
329        let url = format!("{}/v1/messages", self.base_url);
330        let text = self.post_with_retry(&url, &body).await?;
331        let parsed: AnthropicResponse = serde_json::from_str(&text)
332            .map_err(|e| self.make_err(format!("failed to parse response: {e}; body: {text}")))?;
333        Ok(parse_completion(parsed))
334    }
335
336    async fn stream(
337        &self,
338        messages: &[Message],
339        tools: &[ToolSpec],
340        stream_tx: Option<StreamSender>,
341    ) -> Result<Completion> {
342        self.stream_inner(messages, tools, stream_tx).await
343    }
344
345    /// Search-aware completion: model sees eager tools + the
346    /// `ToolSearchTool`; deferred tools are sent with
347    /// `defer_loading: true` and only become callable after the
348    /// model asks for them via `ToolSearchTool`.
349    async fn complete_with_search(
350        &self,
351        messages: &[Message],
352        eager_tools: &[(ToolSpec, Option<String>)],
353        deferred_tools: &[(ToolSpec, Option<String>)],
354    ) -> Result<Completion> {
355        self.run_search_aware_loop(messages, eager_tools, deferred_tools, None, 0)
356            .await
357    }
358}
359
360impl AnthropicProvider {
361    /// Internal streaming implementation.
362    async fn stream_inner(
363        &self,
364        messages: &[Message],
365        tools: &[ToolSpec],
366        stream_tx: Option<StreamSender>,
367    ) -> Result<Completion> {
368        let (system, messages) = extract_system_message(messages);
369        let messages = filter_leading_assistant(&messages);
370
371        let mut body = build_request(
372            &self.model,
373            self.temperature,
374            self.max_tokens,
375            system.as_deref(),
376            &messages,
377            tools,
378        );
379        body["stream"] = Value::Bool(true);
380
381        let url = format!("{}/v1/messages", self.base_url);
382
383        let mut attempt = 0;
384        loop {
385            tracing::debug!(target: "recursive::llm", request = %body, "POST {} (stream)", url);
386            let result = self
387                .client
388                .post(&url)
389                .header("x-api-key", &self.api_key)
390                .header("anthropic-version", "2023-06-01")
391                .header("content-type", "application/json")
392                .json(&body)
393                .send()
394                .await;
395
396            match result {
397                Ok(resp) => {
398                    let status = resp.status();
399                    if status.is_success() {
400                        return self.parse_sse_stream(resp, stream_tx.clone()).await;
401                    }
402
403                    // Non-2xx: read body and check retry
404                    let text = resp.text().await?;
405                    tracing::debug!(target: "recursive::llm", body = %text, "error response (stream)");
406
407                    if let Some(backoff) =
408                        self.retry
409                            .backoff_for(attempt, Some(status.as_u16()), false)
410                    {
411                        tracing::warn!(
412                            target: "recursive::llm",
413                            attempt,
414                            backoff_ms = backoff.as_millis(),
415                            status = status.as_u16(),
416                            "transient HTTP error, retrying (stream)"
417                        );
418                        tokio::time::sleep(backoff).await;
419                        attempt += 1;
420                        continue;
421                    }
422
423                    return Err(self.make_err(format!("HTTP {}: {}", status, text)));
424                }
425                Err(e) => {
426                    if let Some(backoff) = self.retry.backoff_for(attempt, None, true) {
427                        tracing::warn!(
428                            target: "recursive::llm",
429                            attempt,
430                            backoff_ms = backoff.as_millis(),
431                            error = %e,
432                            "network error, retrying (stream)"
433                        );
434                        tokio::time::sleep(backoff).await;
435                        attempt += 1;
436                        continue;
437                    }
438
439                    return Err(self.make_err(format!("request failed: {e}")));
440                }
441            }
442        }
443    }
444
445    /// Parse an Anthropic SSE stream from a successful HTTP response.
446    ///
447    /// Anthropic SSE format:
448    ///   event: message_start\n
449    ///   data: {"type":"message_start","message":{...}}\n\n
450    ///   event: content_block_start\n
451    ///   data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":"Hello"}}\n\n
452    ///   event: content_block_delta\n
453    ///   data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" World"}}\n\n
454    ///   event: message_delta\n
455    ///   data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}\n\n
456    ///   event: message_stop\n
457    ///   data: {"type":"message_stop"}\n\n
458    async fn parse_sse_stream(
459        &self,
460        resp: reqwest::Response,
461        stream_tx: Option<StreamSender>,
462    ) -> Result<Completion> {
463        let mut content = String::new();
464        let mut tool_calls: Vec<StreamToolCall> = Vec::new();
465        let mut finish_reason: Option<String> = None;
466        let mut usage: Option<TokenUsage> = None;
467        let mut input_tokens: Option<u32> = None;
468        let mut output_tokens: Option<u32> = None;
469        let mut cache_creation: Option<u32> = None;
470        let mut cache_read: Option<u32> = None;
471
472        // Read the full response body as text and parse line by line
473        let reader = resp.text().await?;
474        let mut current_event: Option<String> = None;
475
476        for line in reader.lines() {
477            if let Some(event_name) = line.strip_prefix("event: ") {
478                current_event = Some(event_name.to_string());
479                continue;
480            }
481
482            if let Some(data) = line.strip_prefix("data: ") {
483                let event_type = current_event.as_deref().unwrap_or("unknown");
484
485                match event_type {
486                    "message_start" => {
487                        // Extract input tokens from the message
488                        let parsed: Value = serde_json::from_str(data).map_err(|e| {
489                            self.make_err(format!(
490                                "SSE parse error (message_start): {e}; data: {data}"
491                            ))
492                        })?;
493                        if let Some(msg) = parsed.get("message") {
494                            if let Some(u) = msg.get("usage") {
495                                input_tokens = u
496                                    .get("input_tokens")
497                                    .and_then(|v| v.as_u64())
498                                    .map(|v| v as u32);
499                                cache_creation = u
500                                    .get("cache_creation_input_tokens")
501                                    .and_then(|v| v.as_u64())
502                                    .map(|v| v as u32);
503                                cache_read = u
504                                    .get("cache_read_input_tokens")
505                                    .and_then(|v| v.as_u64())
506                                    .map(|v| v as u32);
507                            }
508                        }
509                    }
510                    "content_block_start" => {
511                        let parsed: Value = serde_json::from_str(data).map_err(|e| {
512                            self.make_err(format!(
513                                "SSE parse error (content_block_start): {e}; data: {data}"
514                            ))
515                        })?;
516                        if let Some(block) = parsed.get("content_block") {
517                            let block_type =
518                                block.get("type").and_then(|v| v.as_str()).unwrap_or("");
519                            let index =
520                                parsed.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
521                            match block_type {
522                                "tool_use" => {
523                                    let id = block
524                                        .get("id")
525                                        .and_then(|v| v.as_str())
526                                        .unwrap_or("")
527                                        .to_string();
528                                    let name = block
529                                        .get("name")
530                                        .and_then(|v| v.as_str())
531                                        .unwrap_or("")
532                                        .to_string();
533                                    // Ensure the vec is large enough
534                                    while tool_calls.len() <= index {
535                                        tool_calls.push(StreamToolCall::default());
536                                    }
537                                    tool_calls[index].id = id;
538                                    tool_calls[index].name = name;
539                                }
540                                "text" => {
541                                    // Initial text content (if any)
542                                    if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
543                                        if !text.is_empty() {
544                                            content.push_str(text);
545                                            if let Some(ref tx) = stream_tx {
546                                                let _ = tx.send(text.to_string());
547                                            }
548                                        }
549                                    }
550                                }
551                                _ => {}
552                            }
553                        }
554                    }
555                    "content_block_delta" => {
556                        let parsed: Value = serde_json::from_str(data).map_err(|e| {
557                            self.make_err(format!(
558                                "SSE parse error (content_block_delta): {e}; data: {data}"
559                            ))
560                        })?;
561                        if let Some(delta) = parsed.get("delta") {
562                            let delta_type =
563                                delta.get("type").and_then(|v| v.as_str()).unwrap_or("");
564                            match delta_type {
565                                "text_delta" => {
566                                    if let Some(text) = delta.get("text").and_then(|v| v.as_str()) {
567                                        if !text.is_empty() {
568                                            content.push_str(text);
569                                            if let Some(ref tx) = stream_tx {
570                                                let _ = tx.send(text.to_string());
571                                            }
572                                        }
573                                    }
574                                }
575                                "input_json_delta" => {
576                                    if let Some(partial) =
577                                        delta.get("partial_json").and_then(|v| v.as_str())
578                                    {
579                                        let index = parsed
580                                            .get("index")
581                                            .and_then(|v| v.as_u64())
582                                            .unwrap_or(0)
583                                            as usize;
584                                        while tool_calls.len() <= index {
585                                            tool_calls.push(StreamToolCall::default());
586                                        }
587                                        tool_calls[index].partial_json.push_str(partial);
588                                    }
589                                }
590                                _ => {}
591                            }
592                        }
593                    }
594                    "message_delta" => {
595                        let parsed: Value = serde_json::from_str(data).map_err(|e| {
596                            self.make_err(format!(
597                                "SSE parse error (message_delta): {e}; data: {data}"
598                            ))
599                        })?;
600                        if let Some(delta) = parsed.get("delta") {
601                            if let Some(reason) = delta.get("stop_reason").and_then(|v| v.as_str())
602                            {
603                                finish_reason = Some(reason.to_string());
604                            }
605                        }
606                        if let Some(u) = parsed.get("usage") {
607                            output_tokens = u
608                                .get("output_tokens")
609                                .and_then(|v| v.as_u64())
610                                .map(|v| v as u32);
611                        }
612                    }
613                    "message_stop" => {
614                        // End of stream - nothing to extract
615                    }
616                    "ping" => {
617                        // Anthropic sends periodic pings to keep the connection alive
618                    }
619                    _ => {
620                        tracing::debug!(target: "recursive::llm", event = %event_type, data = %data, "unhandled SSE event");
621                    }
622                }
623
624                current_event = None;
625            }
626        }
627
628        // Build final TokenUsage from accumulated fields
629        if input_tokens.is_some() || output_tokens.is_some() {
630            let prompt = input_tokens.unwrap_or(0);
631            let completion = output_tokens.unwrap_or(0);
632            usage = Some(TokenUsage {
633                prompt_tokens: prompt,
634                completion_tokens: completion,
635                total_tokens: prompt.saturating_add(completion),
636                cache_hit_tokens: cache_read.unwrap_or(0),
637                cache_miss_tokens: cache_creation.unwrap_or(0),
638            });
639        }
640
641        // Convert streamed tool calls to final ToolCall objects
642        let final_tool_calls: Vec<ToolCall> = tool_calls
643            .into_iter()
644            .filter(|tc| !tc.id.is_empty())
645            .map(|tc| {
646                let args: Value = if tc.partial_json.trim().is_empty() {
647                    Value::Object(Default::default())
648                } else {
649                    serde_json::from_str(&tc.partial_json)
650                        .unwrap_or_else(|_| Value::String(tc.partial_json.clone()))
651                };
652                ToolCall {
653                    id: tc.id,
654                    name: tc.name,
655                    arguments: args,
656                }
657            })
658            .collect();
659
660        Ok(Completion {
661            content,
662            tool_calls: final_tool_calls,
663            finish_reason: finish_reason.map(|r| match r.as_str() {
664                "end_turn" => "stop".to_string(),
665                "max_tokens" => "length".to_string(),
666                "tool_use" => "tool_calls".to_string(),
667                other => other.to_string(),
668            }),
669            usage,
670            reasoning_content: None,
671        })
672    }
673}
674
675/// Extract system message if present, return (system_content, remaining_messages).
676fn extract_system_message(messages: &[Message]) -> (Option<String>, Vec<Message>) {
677    if messages.first().is_some_and(|m| m.role == Role::System) {
678        let system = messages[0].content.clone();
679        let rest = messages[1..].to_vec();
680        (Some(system), rest)
681    } else {
682        (None, messages.to_vec())
683    }
684}
685
686/// Filter out any leading assistant messages that would cause Anthropic to reject the request.
687/// Anthropic requires the first message to be user, or system+user.
688fn filter_leading_assistant(messages: &[Message]) -> Vec<Message> {
689    let mut result = messages.to_vec();
690    while result
691        .first()
692        .is_some_and(|m| m.role == Role::Assistant && m.tool_calls.is_empty())
693    {
694        // Remove leading assistant message with no tool calls
695        result.remove(0);
696    }
697    result
698}
699
700fn build_request(
701    model: &str,
702    temperature: f64,
703    max_tokens: u32,
704    system: Option<&str>,
705    messages: &[Message],
706    tools: &[ToolSpec],
707) -> Value {
708    let mut req = serde_json::json!({
709        "model": model,
710        "max_tokens": max_tokens,
711        "temperature": temperature,
712    });
713
714    if let Some(sys) = system {
715        req["system"] = Value::String(sys.to_string());
716    }
717
718    let msgs: Vec<Value> = messages.iter().map(serialize_message).collect();
719    req["messages"] = Value::Array(msgs);
720
721    if !tools.is_empty() {
722        let tools_json: Vec<Value> = tools
723            .iter()
724            .map(|t| {
725                serde_json::json!({
726                    "name": t.name,
727                    "description": t.description,
728                    "input_schema": t.parameters,
729                })
730            })
731            .collect();
732        req["tools"] = Value::Array(tools_json);
733    }
734
735    req
736}
737
738/// Build a request body that splits tools into an eager list (no
739/// `defer_loading`) and a deferred list (each marked
740/// `defer_loading: true`). Used by the search-aware completion
741/// path so the model can `ToolSearchTool` the deferred tools'
742/// schemas on demand.
743///
744/// When `pending_tool_result` is `Some((tool_use_id, names))`, the
745/// last message in `messages` (which is a sentinel user message
746/// whose `tool_call_id` matches `tool_use_id`) is rewritten into a
747/// proper `tool_result` content block whose `content` is an array
748/// of `tool_reference` items. This is how the Anthropic API
749/// receives the "tool resolved" signal.
750#[allow(clippy::too_many_arguments)]
751fn build_request_with_partition(
752    model: &str,
753    temperature: f64,
754    max_tokens: u32,
755    system: Option<&str>,
756    messages: &[Message],
757    eager_tools: &[SpecWithHint],
758    deferred_tools: &[SpecWithHint],
759    pending_tool_result: Option<(&str, &[String])>,
760) -> Value {
761    let mut req = serde_json::json!({
762        "model": model,
763        "max_tokens": max_tokens,
764        "temperature": temperature,
765    });
766
767    if let Some(sys) = system {
768        req["system"] = Value::String(sys.to_string());
769    }
770
771    let mut msgs: Vec<Value> = messages.iter().map(serialize_message).collect();
772    if let Some((id, names)) = pending_tool_result {
773        if let Some(last) = msgs.last_mut() {
774            // Replace the last (marker) message with a proper
775            // tool_result block. The marker is a user message
776            // with `tool_call_id = Some(id)`, so its
777            // serialized form already has role=user and
778            // content=[{type:tool_result,...}] — but the
779            // `content` field of that block is just a string.
780            // We need it to be an *array* of
781            // `tool_reference` items, so we overwrite
782            // wholesale.
783            *last = serde_json::json!({
784                "role": "user",
785                "content": [{
786                    "type": "tool_result",
787                    "tool_use_id": id,
788                    "content": tool_reference_array(names),
789                }],
790            });
791        }
792    }
793    req["messages"] = Value::Array(msgs);
794
795    if !eager_tools.is_empty() || !deferred_tools.is_empty() {
796        let mut tools_json: Vec<Value> =
797            Vec::with_capacity(eager_tools.len() + deferred_tools.len());
798        for (spec, _) in eager_tools {
799            tools_json.push(serde_json::json!({
800                "name": spec.name,
801                "description": spec.description,
802                "input_schema": spec.parameters,
803            }));
804        }
805        for (spec, _) in deferred_tools {
806            tools_json.push(serde_json::json!({
807                "name": spec.name,
808                "description": spec.description,
809                "input_schema": spec.parameters,
810                "defer_loading": true,
811            }));
812        }
813        req["tools"] = Value::Array(tools_json);
814    }
815
816    req
817}
818
819/// Build the JSON array of `tool_reference` content blocks that
820/// goes inside a `tool_result.content` field.
821fn tool_reference_array(names: &[String]) -> Value {
822    Value::Array(
823        names
824            .iter()
825            .map(|n| {
826                serde_json::json!({
827                    "type": "tool_reference",
828                    "tool_name": n,
829                })
830            })
831            .collect(),
832    )
833}
834
835fn serialize_message(m: &Message) -> Value {
836    let role = match m.role {
837        Role::System => "system",
838        Role::User => "user",
839        Role::Assistant => "assistant",
840        Role::Tool => "user", // Anthropic uses "user" for tool results
841    };
842
843    // If this message has tool calls, format as tool_use blocks
844    if !m.tool_calls.is_empty() {
845        let tool_uses: Vec<Value> = m
846            .tool_calls
847            .iter()
848            .map(|c| {
849                serde_json::json!({
850                    "type": "tool_use",
851                    "id": c.id,
852                    "name": c.name,
853                    "input": c.arguments,
854                })
855            })
856            .collect();
857        serde_json::json!({
858            "role": role,
859            "content": tool_uses,
860        })
861    } else if let Some(id) = &m.tool_call_id {
862        // Tool result message - format as tool_result content block
863        serde_json::json!({
864            "role": role,
865            "content": [{
866                "type": "tool_result",
867                "tool_use_id": id,
868                "content": m.content,
869            }],
870        })
871    } else {
872        // Regular text message
873        serde_json::json!({
874            "role": role,
875            "content": m.content,
876        })
877    }
878}
879
880#[derive(Debug, Deserialize)]
881struct AnthropicResponse {
882    #[serde(default)]
883    content: Vec<ContentBlock>,
884    #[serde(default)]
885    usage: Option<AnthropicUsage>,
886    #[serde(default)]
887    stop_reason: Option<String>,
888}
889
890#[derive(Debug, Deserialize)]
891#[serde(tag = "type")]
892enum ContentBlock {
893    #[serde(rename = "text")]
894    Text {
895        #[serde(default)]
896        text: String,
897    },
898    #[serde(rename = "tool_use")]
899    ToolUse {
900        id: String,
901        name: String,
902        #[serde(default)]
903        input: Value,
904    },
905}
906
907#[derive(Debug, Deserialize)]
908struct AnthropicUsage {
909    #[serde(default)]
910    input_tokens: Option<u32>,
911    #[serde(default)]
912    output_tokens: Option<u32>,
913    #[serde(default)]
914    cache_creation_input_tokens: Option<u32>,
915    #[serde(default)]
916    cache_read_input_tokens: Option<u32>,
917}
918
919impl AnthropicUsage {
920    fn to_token_usage(&self) -> TokenUsage {
921        TokenUsage {
922            prompt_tokens: self.input_tokens.unwrap_or(0),
923            completion_tokens: self.output_tokens.unwrap_or(0),
924            total_tokens: self
925                .input_tokens
926                .unwrap_or(0)
927                .saturating_add(self.output_tokens.unwrap_or(0)),
928            cache_hit_tokens: self.cache_read_input_tokens.unwrap_or(0),
929            cache_miss_tokens: self.cache_creation_input_tokens.unwrap_or(0),
930        }
931    }
932}
933
934/// Accumulator for tool calls being built from SSE stream events.
935#[derive(Default)]
936struct StreamToolCall {
937    id: String,
938    name: String,
939    partial_json: String,
940}
941
942fn parse_completion(response: AnthropicResponse) -> Completion {
943    let mut content = String::new();
944    let mut tool_calls = Vec::new();
945
946    for block in response.content {
947        match block {
948            ContentBlock::Text { text } => {
949                content.push_str(&text);
950            }
951            ContentBlock::ToolUse { id, name, input } => {
952                tool_calls.push(ToolCall {
953                    id,
954                    name,
955                    arguments: input,
956                });
957            }
958        }
959    }
960
961    let finish_reason = response.stop_reason.map(|r| match r.as_str() {
962        "end_turn" => "stop".to_string(),
963        "max_tokens" => "length".to_string(),
964        "tool_use" => "tool_calls".to_string(),
965        other => other.to_string(),
966    });
967
968    Completion {
969        content,
970        tool_calls,
971        finish_reason,
972        usage: response.usage.map(|u| u.to_token_usage()),
973        reasoning_content: None,
974    }
975}
976
977#[cfg(test)]
978mod tests {
979    use super::*;
980
981    #[test]
982    fn extract_system_message_when_present() {
983        let msgs = vec![
984            Message::system("You are helpful.".to_string()),
985            Message::user("Hello".to_string()),
986        ];
987        let (system, rest) = extract_system_message(&msgs);
988        assert_eq!(system, Some("You are helpful.".to_string()));
989        assert_eq!(rest.len(), 1);
990        assert_eq!(rest[0].role, Role::User);
991    }
992
993    #[test]
994    fn extract_system_message_when_not_present() {
995        let msgs = vec![Message::user("Hello".to_string())];
996        let (system, rest) = extract_system_message(&msgs);
997        assert_eq!(system, None);
998        assert_eq!(rest.len(), 1);
999    }
1000
1001    #[test]
1002    fn filter_leading_assistant_removes_empty_assistant() {
1003        let msgs = vec![
1004            Message::assistant("Hi".to_string()),
1005            Message::user("Hello".to_string()),
1006        ];
1007        let filtered = filter_leading_assistant(&msgs);
1008        assert_eq!(filtered.len(), 1);
1009        assert_eq!(filtered[0].role, Role::User);
1010    }
1011
1012    #[test]
1013    fn filter_leading_assistant_keeps_assistant_with_tool_calls() {
1014        let msgs = vec![
1015            Message::assistant_with_tool_calls(
1016                "I'll use a tool".to_string(),
1017                vec![ToolCall {
1018                    id: "call_1".to_string(),
1019                    name: "test".to_string(),
1020                    arguments: Value::Object(Default::default()),
1021                }],
1022            ),
1023            Message::user("Hello".to_string()),
1024        ];
1025        let filtered = filter_leading_assistant(&msgs);
1026        // Should keep the assistant message because it has tool_calls
1027        assert_eq!(filtered.len(), 2);
1028    }
1029
1030    #[test]
1031    fn parses_text_response() {
1032        let raw = r#"{
1033            "type": "message",
1034            "content": [{"type": "text", "text": "Hello"}],
1035            "stop_reason": "end_turn",
1036            "usage": {"input_tokens": 10, "output_tokens": 5}
1037        }"#;
1038        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1039        let c = parse_completion(parsed);
1040        assert_eq!(c.content, "Hello");
1041        assert!(c.tool_calls.is_empty());
1042        assert_eq!(c.finish_reason, Some("stop".to_string()));
1043        assert!(c.usage.is_some());
1044    }
1045
1046    #[test]
1047    fn parses_tool_use_response() {
1048        let raw = r#"{
1049            "type": "message",
1050            "content": [
1051                {"type": "tool_use", "id": "call_1", "name": "read_file", "input": {"path": "src/lib.rs"}}
1052            ],
1053            "stop_reason": "tool_use",
1054            "usage": {"input_tokens": 100, "output_tokens": 50}
1055        }"#;
1056        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1057        let c = parse_completion(parsed);
1058        assert!(c.content.is_empty());
1059        assert_eq!(c.tool_calls.len(), 1);
1060        assert_eq!(c.tool_calls[0].id, "call_1");
1061        assert_eq!(c.tool_calls[0].name, "read_file");
1062        assert_eq!(c.tool_calls[0].arguments["path"], "src/lib.rs");
1063    }
1064
1065    #[test]
1066    fn parses_mixed_content_response() {
1067        let raw = r#"{
1068            "type": "message",
1069            "content": [
1070                {"type": "text", "text": "I'll read that file. "},
1071                {"type": "tool_use", "id": "call_1", "name": "read_file", "input": {"path": "src/lib.rs"}}
1072            ],
1073            "stop_reason": "tool_use",
1074            "usage": {"input_tokens": 50, "output_tokens": 25}
1075        }"#;
1076        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1077        let c = parse_completion(parsed);
1078        assert_eq!(c.content, "I'll read that file. ");
1079        assert_eq!(c.tool_calls.len(), 1);
1080    }
1081
1082    #[test]
1083    fn parses_usage_correctly() {
1084        let raw = r#"{
1085            "type": "message",
1086            "content": [{"type": "text", "text": "hi"}],
1087            "stop_reason": "end_turn",
1088            "usage": {
1089                "input_tokens": 100,
1090                "output_tokens": 50,
1091                "cache_creation_input_tokens": 20,
1092                "cache_read_input_tokens": 30
1093            }
1094        }"#;
1095        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1096        let c = parse_completion(parsed);
1097        let u = c.usage.unwrap();
1098        assert_eq!(u.prompt_tokens, 100);
1099        assert_eq!(u.completion_tokens, 50);
1100        assert_eq!(u.cache_hit_tokens, 30);
1101        assert_eq!(u.cache_miss_tokens, 20);
1102    }
1103
1104    #[test]
1105    fn serializes_tool_result_message() {
1106        let msg = Message::tool_result("call_123".to_string(), "result content".to_string());
1107        let v = serialize_message(&msg);
1108        assert_eq!(v["role"], "user");
1109        let content = &v["content"][0];
1110        assert_eq!(content["type"], "tool_result");
1111        assert_eq!(content["tool_use_id"], "call_123");
1112        assert_eq!(content["content"], "result content");
1113    }
1114
1115    #[test]
1116    fn serializes_assistant_with_tool_calls() {
1117        let msg = Message::assistant_with_tool_calls(
1118            "I'll use a tool".to_string(),
1119            vec![ToolCall {
1120                id: "abc".to_string(),
1121                name: "write_file".to_string(),
1122                arguments: serde_json::json!({"path": "a", "contents": "b"}),
1123            }],
1124        );
1125        let v = serialize_message(&msg);
1126        assert_eq!(v["role"], "assistant");
1127        let content = &v["content"][0];
1128        assert_eq!(content["type"], "tool_use");
1129        assert_eq!(content["name"], "write_file");
1130    }
1131
1132    #[test]
1133    fn builds_request_with_system() {
1134        let req = build_request(
1135            "claude-3",
1136            0.2,
1137            4096,
1138            Some("You are helpful."),
1139            &[Message::user("Hi".to_string())],
1140            &[],
1141        );
1142        assert_eq!(req["system"], "You are helpful.");
1143        assert_eq!(req["model"], "claude-3");
1144    }
1145
1146    #[test]
1147    fn builds_request_without_system() {
1148        let req = build_request(
1149            "claude-3",
1150            0.2,
1151            4096,
1152            None,
1153            &[Message::user("Hi".to_string())],
1154            &[],
1155        );
1156        assert!(req.get("system").is_none());
1157    }
1158
1159    #[test]
1160    fn builds_request_with_tools() {
1161        let tools = vec![ToolSpec {
1162            name: "test".to_string(),
1163            description: "A test tool".to_string(),
1164            parameters: serde_json::json!({"type": "object"}),
1165        }];
1166        let req = build_request(
1167            "claude-3",
1168            0.2,
1169            4096,
1170            None,
1171            &[Message::user("Hi".to_string())],
1172            &tools,
1173        );
1174        assert!(req.get("tools").is_some());
1175    }
1176
1177    #[tokio::test]
1178    async fn test_a_mock_server_returns_canned_response() {
1179        // Spawn a mock server that returns a valid Anthropic response
1180        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1181        let addr = listener.local_addr().unwrap();
1182
1183        let handle = std::thread::spawn(move || {
1184            let (mut stream, _) = listener.accept().unwrap();
1185            use std::io::{Read, Write};
1186            let mut buf = [0u8; 4096];
1187            let _ = stream.read(&mut buf);
1188
1189            let body = r#"{"type":"message","content":[{"type":"text","text":"Hello from Anthropic"}],"stop_reason":"end_turn","usage":{"input_tokens":10,"output_tokens":5}}"#;
1190            let response = format!(
1191                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1192                body.len(),
1193                body
1194            );
1195            write!(stream, "{}", response).unwrap();
1196            stream.flush().unwrap();
1197        });
1198
1199        // Give the server a moment to start
1200        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1201
1202        let provider =
1203            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1204        let result = provider
1205            .complete(&[Message::user("hi".to_string())], &[])
1206            .await;
1207
1208        let _ = handle.join();
1209
1210        let completion = result.expect("should succeed");
1211        assert!(completion.content.contains("Hello from Anthropic"));
1212        assert!(completion.usage.is_some());
1213        let u = completion.usage.unwrap();
1214        assert_eq!(u.prompt_tokens, 10);
1215        assert_eq!(u.completion_tokens, 5);
1216    }
1217
1218    #[tokio::test]
1219    async fn test_b_error_response_includes_model_name() {
1220        // Spawn a mock server that returns 401
1221        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1222        let addr = listener.local_addr().unwrap();
1223
1224        let handle = std::thread::spawn(move || {
1225            let (mut stream, _) = listener.accept().unwrap();
1226            use std::io::{Read, Write};
1227            let mut buf = [0u8; 4096];
1228            let _ = stream.read(&mut buf);
1229
1230            let body = r#"{"error":{"type":"authentication_error","message":"Invalid API Key"}}"#;
1231            let response = format!(
1232                "HTTP/1.1 401 Unauthorized\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1233                body.len(),
1234                body
1235            );
1236            write!(stream, "{}", response).unwrap();
1237            stream.flush().unwrap();
1238        });
1239
1240        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1241
1242        let provider =
1243            AnthropicProvider::new(format!("http://{addr}"), "sk-invalid", "claude-3-opus");
1244        let err = provider
1245            .complete(&[Message::user("hi".to_string())], &[])
1246            .await
1247            .unwrap_err();
1248
1249        let _ = handle.join();
1250
1251        let msg = err.to_string();
1252        assert!(
1253            msg.contains("claude-3-opus"),
1254            "error should contain model name: {msg}"
1255        );
1256        assert!(
1257            msg.contains("401"),
1258            "error should contain HTTP status: {msg}"
1259        );
1260    }
1261
1262    #[tokio::test]
1263    async fn test_c_tool_use_extracts_correct_tool_call() {
1264        // Spawn a mock server that returns a tool_use response
1265        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1266        let addr = listener.local_addr().unwrap();
1267
1268        let handle = std::thread::spawn(move || {
1269            let (mut stream, _) = listener.accept().unwrap();
1270            use std::io::{Read, Write};
1271            let mut buf = [0u8; 4096];
1272            let _ = stream.read(&mut buf);
1273
1274            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}}"#;
1275            let response = format!(
1276                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1277                body.len(),
1278                body
1279            );
1280            write!(stream, "{}", response).unwrap();
1281            stream.flush().unwrap();
1282        });
1283
1284        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1285
1286        let provider =
1287            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1288        let result = provider
1289            .complete(&[Message::user("read the file".to_string())], &[])
1290            .await;
1291
1292        let _ = handle.join();
1293
1294        let completion = result.expect("should succeed");
1295        assert_eq!(completion.tool_calls.len(), 1);
1296        let tc = &completion.tool_calls[0];
1297        assert_eq!(tc.id, "call_abc123");
1298        assert_eq!(tc.name, "read_file");
1299        assert_eq!(tc.arguments["path"], "src/lib.rs");
1300    }
1301
1302    #[tokio::test]
1303    async fn test_d_stream_request_includes_stream_true() {
1304        // Spawn a mock server that captures the request body and returns
1305        // a valid SSE response. Assert the request body contains "stream": true.
1306        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1307        let addr = listener.local_addr().unwrap();
1308        let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
1309        let captured_clone = captured.clone();
1310
1311        std::thread::spawn(move || {
1312            let (mut stream, _) = listener.accept().unwrap();
1313            use std::io::{Read, Write};
1314            let mut buf = [0u8; 8192];
1315            let n = stream.read(&mut buf).unwrap();
1316            let request = String::from_utf8_lossy(&buf[..n]).to_string();
1317            if let Some(body_start) = request.find("\r\n\r\n") {
1318                let body = request[body_start + 4..].trim().to_string();
1319                *captured_clone.lock().unwrap() = body.clone();
1320            }
1321            // Return a valid SSE stream (just message_stop to end quickly)
1322            let body = "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
1323            write!(
1324                stream,
1325                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1326                body.len(),
1327                body,
1328            ).unwrap();
1329            stream.flush().unwrap();
1330        });
1331
1332        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1333
1334        let provider =
1335            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1336        let _ = provider
1337            .stream(&[Message::user("hi".to_string())], &[], None)
1338            .await;
1339
1340        let body_str = captured.lock().unwrap().clone();
1341        let body: serde_json::Value = serde_json::from_str(&body_str).unwrap();
1342        assert_eq!(body["stream"], true);
1343    }
1344
1345    #[tokio::test]
1346    async fn test_e_stream_text_deltas_accumulate() {
1347        // Spawn a mock server that sends text deltas via SSE.
1348        // Assert the returned Completion's content is the concatenation.
1349        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1350        let addr = listener.local_addr().unwrap();
1351
1352        std::thread::spawn(move || {
1353            let (mut stream, _) = listener.accept().unwrap();
1354            use std::io::{Read, Write};
1355            let mut buf = [0u8; 4096];
1356            let _ = stream.read(&mut buf);
1357            let body = "\
1358event: message_start
1359data: {\"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}}}
1360
1361event: content_block_start
1362data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1363
1364event: content_block_delta
1365data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" \"}}
1366
1367event: content_block_delta
1368data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"World\"}}
1369
1370event: message_delta
1371data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1372
1373event: message_stop
1374data: {\"type\":\"message_stop\"}
1375
1376";
1377            write!(
1378                stream,
1379                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1380                body.len(),
1381                body,
1382            ).unwrap();
1383            stream.flush().unwrap();
1384        });
1385
1386        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1387
1388        let provider =
1389            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1390        let completion = provider
1391            .stream(&[Message::user("hi".to_string())], &[], None)
1392            .await
1393            .unwrap();
1394        assert_eq!(completion.content, "Hello World");
1395        assert_eq!(completion.finish_reason.as_deref(), Some("stop"));
1396        assert!(completion.usage.is_some());
1397        let u = completion.usage.unwrap();
1398        assert_eq!(u.prompt_tokens, 10);
1399        assert_eq!(u.completion_tokens, 5);
1400    }
1401
1402    #[tokio::test]
1403    async fn test_f_stream_tool_use_assembles_tool_calls() {
1404        // Spawn a mock server that sends tool_use blocks via SSE.
1405        // Assert the returned Completion has the correct tool_calls.
1406        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1407        let addr = listener.local_addr().unwrap();
1408
1409        std::thread::spawn(move || {
1410            let (mut stream, _) = listener.accept().unwrap();
1411            use std::io::{Read, Write};
1412            let mut buf = [0u8; 4096];
1413            let _ = stream.read(&mut buf);
1414            let body = "\
1415event: message_start
1416data: {\"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}}}
1417
1418event: content_block_start
1419data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_abc123\",\"name\":\"read_file\",\"input\":{}}}
1420
1421event: content_block_delta
1422data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\":\\\"src/\"}}
1423
1424event: content_block_delta
1425data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"lib.rs\\\"}\"}}
1426
1427event: message_delta
1428data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":30}}
1429
1430event: message_stop
1431data: {\"type\":\"message_stop\"}
1432
1433";
1434            write!(
1435                stream,
1436                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1437                body.len(),
1438                body,
1439            ).unwrap();
1440            stream.flush().unwrap();
1441        });
1442
1443        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1444
1445        let provider =
1446            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1447        let completion = provider
1448            .stream(&[Message::user("read the file".to_string())], &[], None)
1449            .await
1450            .unwrap();
1451        assert_eq!(completion.tool_calls.len(), 1);
1452        let tc = &completion.tool_calls[0];
1453        assert_eq!(tc.id, "call_abc123");
1454        assert_eq!(tc.name, "read_file");
1455        assert_eq!(tc.arguments["path"], "src/lib.rs");
1456        assert_eq!(completion.finish_reason.as_deref(), Some("tool_calls"));
1457    }
1458
1459    #[tokio::test]
1460    async fn test_g_stream_tx_receives_text_chunks() {
1461        // Spawn a mock server that sends text deltas via SSE.
1462        // Assert the stream_tx channel receives incremental chunks.
1463        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1464        let addr = listener.local_addr().unwrap();
1465
1466        std::thread::spawn(move || {
1467            let (mut stream, _) = listener.accept().unwrap();
1468            use std::io::{Read, Write};
1469            let mut buf = [0u8; 4096];
1470            let _ = stream.read(&mut buf);
1471            let body = "\
1472event: message_start
1473data: {\"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}}}
1474
1475event: content_block_start
1476data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"A\"}}
1477
1478event: content_block_delta
1479data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"B\"}}
1480
1481event: content_block_delta
1482data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"C\"}}
1483
1484event: message_delta
1485data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":3}}
1486
1487event: message_stop
1488data: {\"type\":\"message_stop\"}
1489
1490";
1491            write!(
1492                stream,
1493                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1494                body.len(),
1495                body,
1496            ).unwrap();
1497            stream.flush().unwrap();
1498        });
1499
1500        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1501
1502        let provider =
1503            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1504        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1505        let completion = provider
1506            .stream(&[Message::user("hi".to_string())], &[], Some(tx))
1507            .await
1508            .unwrap();
1509        assert_eq!(completion.content, "ABC");
1510
1511        // Should have received 3 deltas
1512        let mut deltas = Vec::new();
1513        while let Some(d) = rx.recv().await {
1514            deltas.push(d);
1515            if deltas.len() >= 3 {
1516                break;
1517            }
1518        }
1519        assert_eq!(deltas, vec!["A", "B", "C"]);
1520    }
1521
1522    #[tokio::test]
1523    async fn test_h_stream_with_end_turn_produces_stop_reason() {
1524        // Spawn a mock server that sends a complete SSE stream with
1525        // end_turn stop_reason. Assert the Completion has finish_reason "stop".
1526        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1527        let addr = listener.local_addr().unwrap();
1528
1529        std::thread::spawn(move || {
1530            let (mut stream, _) = listener.accept().unwrap();
1531            use std::io::{Read, Write};
1532            let mut buf = [0u8; 4096];
1533            let _ = stream.read(&mut buf);
1534            let body = "\
1535event: message_start
1536data: {\"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}}}
1537
1538event: content_block_start
1539data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1540
1541event: message_delta
1542data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1543
1544event: message_stop
1545data: {\"type\":\"message_stop\"}
1546
1547";
1548            write!(
1549                stream,
1550                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1551                body.len(),
1552                body,
1553            ).unwrap();
1554            stream.flush().unwrap();
1555        });
1556
1557        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1558
1559        let provider =
1560            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1561        let completion = provider
1562            .stream(&[Message::user("hi".to_string())], &[], None)
1563            .await
1564            .unwrap();
1565        assert_eq!(completion.content, "Hello");
1566        assert_eq!(completion.finish_reason.as_deref(), Some("stop"));
1567        assert!(completion.usage.is_some());
1568    }
1569
1570    #[test]
1571    fn policy_retries_5xx_with_exponential_backoff() {
1572        let policy = RetryPolicy::default();
1573        assert_eq!(
1574            policy.backoff_for(0, Some(503), false),
1575            Some(Duration::from_secs(1))
1576        );
1577        assert_eq!(
1578            policy.backoff_for(1, Some(500), false),
1579            Some(Duration::from_secs(2))
1580        );
1581        assert_eq!(policy.backoff_for(2, Some(500), false), None);
1582    }
1583
1584    #[test]
1585    fn policy_retries_network_errors() {
1586        let policy = RetryPolicy::default();
1587        assert_eq!(
1588            policy.backoff_for(0, None, true),
1589            Some(Duration::from_secs(1))
1590        );
1591    }
1592
1593    #[test]
1594    fn policy_does_not_retry_4xx() {
1595        let policy = RetryPolicy::default();
1596        assert_eq!(policy.backoff_for(0, Some(400), false), None);
1597        assert_eq!(policy.backoff_for(0, Some(401), false), None);
1598        assert_eq!(policy.backoff_for(0, Some(404), false), None);
1599    }
1600
1601    #[test]
1602    fn build_request_with_partition_marks_deferred_tools() {
1603        let eager = vec![(
1604            ToolSpec {
1605                name: "read_file".to_string(),
1606                description: "Read a file".to_string(),
1607                parameters: json!({"type": "object"}),
1608            },
1609            None,
1610        )];
1611        let deferred = vec![(
1612            ToolSpec {
1613                name: "notebook_edit".to_string(),
1614                description: "Edit a notebook".to_string(),
1615                parameters: json!({"type": "object"}),
1616            },
1617            Some("jupyter".to_string()),
1618        )];
1619        let body = build_request_with_partition(
1620            "claude-3",
1621            0.2,
1622            4096,
1623            None,
1624            &[Message::user("hi")],
1625            &eager,
1626            &deferred,
1627            None,
1628        );
1629
1630        let tools = body["tools"].as_array().expect("tools should be an array");
1631        assert_eq!(tools.len(), 2);
1632        // Eager has no `defer_loading` key.
1633        assert!(tools[0].get("defer_loading").is_none());
1634        assert_eq!(tools[0]["name"], "read_file");
1635        // Deferred has `defer_loading: true`.
1636        assert_eq!(tools[1]["defer_loading"], true);
1637        assert_eq!(tools[1]["name"], "notebook_edit");
1638    }
1639
1640    #[test]
1641    fn build_request_with_partition_rewrites_pending_tool_result() {
1642        let eager: Vec<SpecWithHint> = Vec::new();
1643        let deferred: Vec<SpecWithHint> = Vec::new();
1644        let msgs = vec![
1645            Message::user("search for foo"),
1646            // Marker assistant message with a tool_use.
1647            Message::assistant_with_tool_calls(
1648                String::new(),
1649                vec![ToolCall {
1650                    id: "call_xyz".to_string(),
1651                    name: TOOL_SEARCH_TOOL_NAME.to_string(),
1652                    arguments: json!({"query": "foo"}),
1653                }],
1654            ),
1655            // Marker user message — `content` is unused, the
1656            // body builder will overwrite this whole message.
1657            Message::tool_result("call_xyz".to_string(), String::new()),
1658        ];
1659        let names = ["notebook_edit".to_string()];
1660        let pending = Some(("call_xyz", &names[..]));
1661
1662        let body = build_request_with_partition(
1663            "claude-3", 0.2, 4096, None, &msgs, &eager, &deferred, pending,
1664        );
1665
1666        let wire_msgs = body["messages"].as_array().unwrap();
1667        assert_eq!(wire_msgs.len(), 3);
1668        let last = &wire_msgs[2];
1669        assert_eq!(last["role"], "user");
1670        let content = last["content"]
1671            .as_array()
1672            .expect("content should be an array");
1673        assert_eq!(content.len(), 1);
1674        assert_eq!(content[0]["type"], "tool_result");
1675        assert_eq!(content[0]["tool_use_id"], "call_xyz");
1676        let inner = content[0]["content"]
1677            .as_array()
1678            .expect("inner content must be array");
1679        assert_eq!(inner.len(), 1);
1680        assert_eq!(inner[0]["type"], "tool_reference");
1681        assert_eq!(inner[0]["tool_name"], "notebook_edit");
1682    }
1683
1684    #[test]
1685    fn tool_reference_array_emits_one_block_per_name() {
1686        let arr = tool_reference_array(&["a".to_string(), "b".to_string(), "c".to_string()]);
1687        let items = arr.as_array().unwrap();
1688        assert_eq!(items.len(), 3);
1689        for (i, expected) in ["a", "b", "c"].iter().enumerate() {
1690            assert_eq!(items[i]["type"], "tool_reference");
1691            assert_eq!(items[i]["tool_name"], *expected);
1692        }
1693    }
1694
1695    #[tokio::test]
1696    async fn complete_with_search_round_trips_tool_reference() {
1697        // Mock server that:
1698        //   - On request 1, returns a `ToolSearchTool` tool_use.
1699        //   - On request 2, returns a real `read_file` tool_use.
1700        // The test asserts:
1701        //   - The final Completion is the `read_file` call.
1702        //   - The first request's body has ToolSearchTool + a
1703        //     deferred tool marked `defer_loading: true`.
1704        //   - The second request's body has the `tool_result`
1705        //     content block whose `content` is an array of
1706        //     `tool_reference` items.
1707        use std::io::{Read, Write};
1708        use std::sync::{Arc, Mutex};
1709
1710        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1711        let addr = listener.local_addr().unwrap();
1712        let captured: Arc<Mutex<Vec<String>>> = Arc::new(Mutex::new(Vec::new()));
1713
1714        let captured_clone = captured.clone();
1715        std::thread::spawn(move || {
1716            for i in 0..2 {
1717                let (mut stream, _) = listener.accept().unwrap();
1718                let mut buf = [0u8; 16384];
1719                let mut acc = Vec::new();
1720                // Read headers + body. For a small test request,
1721                // the first read should grab everything.
1722                loop {
1723                    let n = stream.read(&mut buf).unwrap();
1724                    if n == 0 {
1725                        break;
1726                    }
1727                    acc.extend_from_slice(&buf[..n]);
1728                    if acc.windows(4).any(|w| w == b"\r\n\r\n") {
1729                        break;
1730                    }
1731                }
1732                let request = String::from_utf8_lossy(&acc).to_string();
1733                let body = request
1734                    .split("\r\n\r\n")
1735                    .nth(1)
1736                    .unwrap_or("")
1737                    .trim()
1738                    .to_string();
1739                captured_clone.lock().unwrap().push(body);
1740
1741                let response_body = if i == 0 {
1742                    // First request: model calls ToolSearchTool
1743                    // looking for the deferred `notebook_edit`.
1744                    r#"{"type":"message","content":[{"type":"tool_use","id":"call_search_1","name":"ToolSearchTool","input":{"query":"jupyter","max_results":3}}],"stop_reason":"tool_use","usage":{"input_tokens":50,"output_tokens":10}}"#
1745                } else {
1746                    // Second request: model now calls the
1747                    // resolved deferred tool.
1748                    r#"{"type":"message","content":[{"type":"tool_use","id":"call_real_1","name":"notebook_edit","input":{"path":"analysis.ipynb"}}],"stop_reason":"tool_use","usage":{"input_tokens":80,"output_tokens":20}}"#
1749                };
1750                let response = format!(
1751                    "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1752                    response_body.len(),
1753                    response_body,
1754                );
1755                stream.write_all(response.as_bytes()).unwrap();
1756                stream.flush().unwrap();
1757            }
1758        });
1759
1760        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1761        let provider =
1762            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet");
1763
1764        // Pretend "read_file" is eager (always available) and
1765        // "notebook_edit" is deferred (needs ToolSearchTool).
1766        let eager = vec![(
1767            ToolSpec {
1768                name: "read_file".to_string(),
1769                description: "Read a UTF-8 text file under the workspace.".to_string(),
1770                parameters: json!({"type": "object"}),
1771            },
1772            None,
1773        )];
1774        let deferred = vec![(
1775            ToolSpec {
1776                name: "notebook_edit".to_string(),
1777                description: "Edit a Jupyter notebook cell.".to_string(),
1778                parameters: json!({"type": "object"}),
1779            },
1780            Some("jupyter notebook".to_string()),
1781        )];
1782
1783        let result = provider
1784            .complete_with_search(&[Message::user("find and read a file")], &eager, &deferred)
1785            .await;
1786
1787        let captured_bodies = captured.lock().unwrap().clone();
1788        assert_eq!(
1789            captured_bodies.len(),
1790            2,
1791            "expected two requests, got {}",
1792            captured_bodies.len()
1793        );
1794
1795        // First request: ToolSearchTool + read_file (eager),
1796        // notebook_edit (deferred).
1797        let body1: serde_json::Value = serde_json::from_str(&captured_bodies[0]).unwrap();
1798        let tools1 = body1["tools"].as_array().expect("tools should be array");
1799        let tool_names: Vec<&str> = tools1
1800            .iter()
1801            .map(|t| t["name"].as_str().unwrap_or(""))
1802            .collect();
1803        assert!(
1804            tool_names.contains(&"ToolSearchTool"),
1805            "missing ToolSearchTool in {:?}",
1806            tool_names
1807        );
1808        assert!(
1809            tool_names.contains(&"read_file"),
1810            "missing read_file in {:?}",
1811            tool_names
1812        );
1813        assert!(
1814            tool_names.contains(&"notebook_edit"),
1815            "missing notebook_edit in {:?}",
1816            tool_names
1817        );
1818        let notebook_def = tools1
1819            .iter()
1820            .find(|t| t["name"] == "notebook_edit")
1821            .unwrap();
1822        assert_eq!(
1823            notebook_def["defer_loading"], true,
1824            "notebook_edit should be deferred: {:?}",
1825            notebook_def
1826        );
1827
1828        // Second request: messages should include the assistant
1829        // tool_use AND a user message with tool_result whose
1830        // content is an array of tool_reference items.
1831        let body2: serde_json::Value = serde_json::from_str(&captured_bodies[1]).unwrap();
1832        let msgs2 = body2["messages"].as_array().unwrap();
1833        assert!(
1834            msgs2.len() >= 3,
1835            "expected at least 3 messages (user, assistant, tool_result), got {}",
1836            msgs2.len()
1837        );
1838        // Last message should be the tool_result with tool_reference.
1839        let last = &msgs2[msgs2.len() - 1];
1840        assert_eq!(last["role"], "user");
1841        let content = last["content"].as_array().expect("content should be array");
1842        let tool_result = content
1843            .iter()
1844            .find(|c| c["type"] == "tool_result")
1845            .expect("expected a tool_result block");
1846        assert_eq!(tool_result["tool_use_id"], "call_search_1");
1847        let refs = tool_result["content"]
1848            .as_array()
1849            .expect("tool_result.content should be an array");
1850        assert!(!refs.is_empty(), "tool_reference array should be non-empty");
1851        for r in refs {
1852            assert_eq!(r["type"], "tool_reference");
1853            assert!(
1854                r["tool_name"].is_string(),
1855                "tool_reference.tool_name should be a string"
1856            );
1857        }
1858
1859        // Final completion should be the notebook_edit call
1860        // (the deferred tool that the model asked to resolve).
1861        let completion = result.expect("should succeed");
1862        assert_eq!(completion.tool_calls.len(), 1);
1863        let tc = &completion.tool_calls[0];
1864        assert_eq!(tc.name, "notebook_edit");
1865        assert_eq!(tc.arguments["path"], "analysis.ipynb");
1866    }
1867}