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::time::Duration;
7
8use async_trait::async_trait;
9use futures_util::StreamExt;
10use reqwest::Client;
11use serde::Deserialize;
12use serde_json::Value;
13
14use super::{
15    ChatProvider, Completion, RetryPolicy, StreamChunk, StreamSender, TokenUsage, ToolCall,
16    ToolSpec,
17};
18
19/// Beta header required for `tool_reference` block support in the
20/// Anthropic Messages API.
21const TOOL_SEARCH_BETA_HEADER: &str = "advanced-tool-use-2025-11-20";
22
23use crate::error::{Error, Result};
24use crate::message::{Message, Role};
25
26#[derive(Debug, Clone)]
27pub struct AnthropicProvider {
28    base_url: String,
29    api_key: String,
30    model: String,
31    client: Client,
32    temperature: f64,
33    max_tokens: u32,
34    retry: RetryPolicy,
35    /// Cap on `ToolSearchTool` round-trips. Set via `with_max_search_rounds()`
36    /// for API parity with `OpenAiProvider`. Not yet consumed because Anthropic
37    /// does not currently have a server-side deferred search loop. Wire it up
38    /// once that becomes available.
39    #[allow(
40        dead_code,
41        reason = "API parity placeholder; used once Anthropic adds deferred search"
42    )]
43    max_search_rounds: usize,
44}
45
46impl AnthropicProvider {
47    pub fn new(
48        base_url: impl Into<String>,
49        api_key: impl Into<String>,
50        model: impl Into<String>,
51    ) -> Result<Self> {
52        let client = Client::builder()
53            .timeout(Duration::from_secs(180))
54            .build()
55            .map_err(|e| Error::Config {
56                message: format!("failed to build HTTP client: {e}"),
57            })?;
58        Ok(Self {
59            base_url: base_url.into().trim_end_matches('/').to_string(),
60            api_key: api_key.into(),
61            model: model.into(),
62            client,
63            temperature: 0.2,
64            // Seed from the crate default (see openai.rs for the rationale:
65            // 4096 was far too small and truncated reasoning turns). The
66            // Anthropic Messages API requires `max_tokens` and enforces a
67            // model-specific ceiling, but 64K is within every current
68            // Claude model's limit and is overridden via `with_max_tokens`
69            // from `Config::from_env` (RECURSIVE_MAX_TOKENS) anyway.
70            max_tokens: crate::llm::DEFAULT_MAX_TOKENS,
71            retry: RetryPolicy::default(),
72            max_search_rounds: 3,
73        })
74    }
75
76    /// Build an `Error::Llm` with the model name prefixed.
77    fn make_err(&self, ctx: impl Into<String>) -> Error {
78        Error::Llm {
79            provider: self.model.clone(),
80            message: ctx.into(),
81        }
82    }
83
84    pub fn with_temperature(mut self, t: f64) -> Self {
85        self.temperature = t;
86        self
87    }
88
89    pub fn with_max_tokens(mut self, n: u32) -> Self {
90        self.max_tokens = n;
91        self
92    }
93
94    pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
95        self.retry = policy;
96        self
97    }
98
99    /// Cap on `ToolSearchTool` round-trips per call. Stored for API parity
100    /// with `OpenAiProvider`; wired up once Anthropic supports deferred search.
101    pub fn with_max_search_rounds(mut self, n: usize) -> Self {
102        self.max_search_rounds = n;
103        self
104    }
105
106    /// POST `body` to `url` with the standard retry policy.
107    async fn post_with_retry(&self, url: &str, body: &Value) -> Result<String> {
108        let mut attempt = 0;
109        loop {
110            tracing::debug!(target: "recursive::llm", request = %body, "POST {}", url);
111            let result = self
112                .client
113                .post(url)
114                .header("x-api-key", &self.api_key)
115                .header("anthropic-version", "2023-06-01")
116                .header("anthropic-beta", TOOL_SEARCH_BETA_HEADER)
117                .header("content-type", "application/json")
118                .json(body)
119                .send()
120                .await;
121            match result {
122                Ok(resp) => {
123                    let status = resp.status();
124                    if status.is_success() {
125                        return resp.text().await.map_err(Error::from);
126                    }
127                    let text = resp.text().await?;
128                    if let Some(backoff) =
129                        self.retry
130                            .backoff_for(attempt, Some(status.as_u16()), false)
131                    {
132                        tracing::warn!(
133                            target: "recursive::llm",
134                            attempt,
135                            backoff_ms = backoff.as_millis(),
136                            status = status.as_u16(),
137                            "transient HTTP error, retrying"
138                        );
139                        tokio::time::sleep(backoff).await;
140                        attempt += 1;
141                        continue;
142                    }
143                    return Err(self.make_err(format!("HTTP {}: {}", status, text)));
144                }
145                Err(e) => {
146                    if let Some(backoff) = self.retry.backoff_for(attempt, None, true) {
147                        tracing::warn!(
148                            target: "recursive::llm",
149                            attempt,
150                            backoff_ms = backoff.as_millis(),
151                            error = %e,
152                            "network error, retrying"
153                        );
154                        tokio::time::sleep(backoff).await;
155                        attempt += 1;
156                        continue;
157                    }
158                    return Err(self.make_err(format!("request failed: {e}")));
159                }
160            }
161        }
162    }
163}
164
165#[async_trait]
166impl ChatProvider for AnthropicProvider {
167    /// Whether to use deferred tool loading via `tool_reference` blocks.
168    ///
169    /// `tool_reference` is an Anthropic beta feature (`advanced-tool-use-2025-11-20`).
170    /// The official `api.anthropic.com` endpoint supports it; third-party
171    /// Anthropic-compatible endpoints (DeepSeek, MiniMax, etc.) typically do not.
172    /// Mirroring Claude Code's `isFirstPartyAnthropicBaseUrl()` check: only enable
173    /// deferred tools when the endpoint is the official Anthropic API.
174    ///
175    /// Users on compatible proxies can set `RECURSIVE_DEFERRED_TOOLS=true` to opt in.
176    fn supports_deferred_tools(&self) -> bool {
177        if std::env::var("RECURSIVE_DEFERRED_TOOLS")
178            .map(|v| v.eq_ignore_ascii_case("true") || v == "1")
179            .unwrap_or(false)
180        {
181            return true;
182        }
183        self.base_url.contains("api.anthropic.com")
184    }
185
186    async fn complete(&self, messages: &[Message], tools: &[ToolSpec]) -> Result<Completion> {
187        let (system, messages) = extract_system_message(messages);
188        let messages = filter_leading_assistant(&messages);
189        let body = build_request(
190            &self.model,
191            self.temperature,
192            self.max_tokens,
193            system.as_deref(),
194            &messages,
195            tools,
196        );
197        let url = format!("{}/v1/messages", self.base_url);
198        let text = self.post_with_retry(&url, &body).await?;
199        let parsed: AnthropicResponse = serde_json::from_str(&text)
200            .map_err(|e| self.make_err(format!("failed to parse response: {e}; body: {text}")))?;
201        Ok(parse_completion(parsed))
202    }
203
204    async fn stream(
205        &self,
206        messages: &[Message],
207        tools: &[ToolSpec],
208        stream_tx: Option<StreamSender>,
209        cancel_token: Option<tokio_util::sync::CancellationToken>,
210    ) -> Result<Completion> {
211        self.stream_inner(messages, tools, stream_tx, cancel_token)
212            .await
213    }
214}
215
216impl AnthropicProvider {
217    /// Send a pre-built request body as a streaming call and return the
218    /// accumulated `Completion`. Handles HTTP retry internally.
219    /// Internal streaming implementation.
220    async fn stream_inner(
221        &self,
222        messages: &[Message],
223        tools: &[ToolSpec],
224        stream_tx: Option<StreamSender>,
225        cancel_token: Option<tokio_util::sync::CancellationToken>,
226    ) -> Result<Completion> {
227        let (system, messages) = extract_system_message(messages);
228        let messages = filter_leading_assistant(&messages);
229
230        let mut body = build_request(
231            &self.model,
232            self.temperature,
233            self.max_tokens,
234            system.as_deref(),
235            &messages,
236            tools,
237        );
238        body["stream"] = Value::Bool(true);
239
240        let url = format!("{}/v1/messages", self.base_url);
241
242        let mut attempt = 0;
243        loop {
244            tracing::debug!(target: "recursive::llm", request = %body, "POST {} (stream)", url);
245            let result = self
246                .client
247                .post(&url)
248                .header("x-api-key", &self.api_key)
249                .header("anthropic-version", "2023-06-01")
250                .header("anthropic-beta", TOOL_SEARCH_BETA_HEADER)
251                .header("content-type", "application/json")
252                .json(&body)
253                .send()
254                .await;
255
256            match result {
257                Ok(resp) => {
258                    let status = resp.status();
259                    if status.is_success() {
260                        return self
261                            .parse_sse_stream(resp, stream_tx.clone(), cancel_token.clone())
262                            .await;
263                    }
264
265                    // Non-2xx: read body and check retry
266                    let text = resp.text().await?;
267                    tracing::debug!(target: "recursive::llm", body = %text, "error response (stream)");
268
269                    if let Some(backoff) =
270                        self.retry
271                            .backoff_for(attempt, Some(status.as_u16()), false)
272                    {
273                        tracing::warn!(
274                            target: "recursive::llm",
275                            attempt,
276                            backoff_ms = backoff.as_millis(),
277                            status = status.as_u16(),
278                            "transient HTTP error, retrying (stream)"
279                        );
280                        tokio::time::sleep(backoff).await;
281                        attempt += 1;
282                        continue;
283                    }
284
285                    return Err(self.make_err(format!("HTTP {}: {}", status, text)));
286                }
287                Err(e) => {
288                    if let Some(backoff) = self.retry.backoff_for(attempt, None, true) {
289                        tracing::warn!(
290                            target: "recursive::llm",
291                            attempt,
292                            backoff_ms = backoff.as_millis(),
293                            error = %e,
294                            "network error, retrying (stream)"
295                        );
296                        tokio::time::sleep(backoff).await;
297                        attempt += 1;
298                        continue;
299                    }
300
301                    return Err(self.make_err(format!("request failed: {e}")));
302                }
303            }
304        }
305    }
306
307    /// Parse an Anthropic SSE stream from a successful HTTP response.
308    ///
309    /// Anthropic SSE format:
310    ///   event: message_start\n
311    ///   data: {"type":"message_start","message":{...}}\n\n
312    ///   event: content_block_start\n
313    ///   data: {"type":"content_block_start","index":0,"content_block":{"type":"text","text":"Hello"}}\n\n
314    ///   event: content_block_delta\n
315    ///   data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" World"}}\n\n
316    ///   event: message_delta\n
317    ///   data: {"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":5}}\n\n
318    ///   event: message_stop\n
319    ///   data: {"type":"message_stop"}\n\n
320    async fn parse_sse_stream(
321        &self,
322        resp: reqwest::Response,
323        stream_tx: Option<StreamSender>,
324        cancel_token: Option<tokio_util::sync::CancellationToken>,
325    ) -> Result<Completion> {
326        // Process the byte stream incrementally so reasoning/text deltas are
327        // forwarded to `stream_tx` as they arrive — not buffered until the
328        // whole response lands (which would make streaming look fake).
329        //
330        // A raw byte buffer (`incomplete`) carries any partial UTF-8 sequence
331        // split across HTTP chunk boundaries; lossy decoding would corrupt
332        // multi-byte (e.g. Chinese) content mid-stream.
333        let mut acc = SseAccum::default();
334        let mut byte_stream = resp.bytes_stream();
335        let mut incomplete: Vec<u8> = Vec::new();
336        let mut line_buf = String::new();
337
338        // 决策 4b:把 SSE 循环改成 tokio::select!,让 cancel_token 能在两个 chunk 之间立刻
339        // 触发。触发后 reqwest::Response 在函数返回时被 drop(HTTP 连接关闭),返回
340        // Err(Error::Cancelled);调用方(run_inner 的 dispatch_llm_step 调用点)会把这个
341        // 错误翻译成 FinishReason::Cancelled(Invariant #7)。
342        loop {
343            let chunk_result = if let Some(ct) = cancel_token.as_ref() {
344                tokio::select! {
345                    biased;
346                    _ = ct.cancelled() => {
347                        // Goal 382: a user-initiated cancel mid-stream should
348                        // still persist whatever content already arrived. If
349                        // anything accumulated, return it as a partial
350                        // Completion (finish_reason "interrupted" sentinel);
351                        // run_inner routes it to FinishReason::Cancelled.
352                        // With nothing accumulated there is nothing to save —
353                        // keep the original Err(Error::Cancelled) so
354                        // run_inner's Cancelled arm handles it.
355                        let have_partial = !acc.content.is_empty()
356                            || !acc.reasoning_content.is_empty()
357                            || !acc.tool_calls.is_empty();
358                        if have_partial {
359                            tracing::warn!(
360                                target: "recursive::llm",
361                                content_len = acc.content.len(),
362                                "SSE stream cancelled; returning partial completion"
363                            );
364                            // Drop half-received tool calls (orphan risk).
365                            acc.tool_calls.clear();
366                            return Ok(Self::build_completion(
367                                acc,
368                                Some("interrupted".to_string()),
369                            ));
370                        }
371                        return Err(Error::Cancelled);
372                    }
373                    next = byte_stream.next() => next,
374                }
375            } else {
376                byte_stream.next().await
377            };
378            let bytes = match chunk_result {
379                Some(Ok(c)) => c,
380                Some(Err(e)) => {
381                    // Goal 382: a network drop mid-stream currently discards
382                    // everything accumulated in `acc`. If any content
383                    // arrived, return it as a partial Completion so the
384                    // caller persists it (the "interrupted" sentinel routes
385                    // to FinishReason::Cancelled). A drop before the first
386                    // token has nothing to save — keep the Err.
387                    let have_partial = !acc.content.is_empty()
388                        || !acc.reasoning_content.is_empty()
389                        || !acc.tool_calls.is_empty();
390                    if have_partial {
391                        tracing::warn!(
392                            target: "recursive::llm",
393                            error = %e,
394                            content_len = acc.content.len(),
395                            "SSE stream interrupted; returning partial completion"
396                        );
397                        // Drop half-received tool calls (orphan risk).
398                        acc.tool_calls.clear();
399                        return Ok(Self::build_completion(acc, Some("interrupted".to_string())));
400                    }
401                    return Err(self.make_err(format!("SSE stream read error: {e}")));
402                }
403                None => break,
404            };
405            let combined: Vec<u8> = if incomplete.is_empty() {
406                bytes.to_vec()
407            } else {
408                let mut v = std::mem::take(&mut incomplete);
409                v.extend_from_slice(&bytes);
410                v
411            };
412            let valid_up_to = match std::str::from_utf8(&combined) {
413                Ok(_) => combined.len(),
414                Err(e) => e.valid_up_to(),
415            };
416            incomplete = combined[valid_up_to..].to_vec();
417            // SAFETY: valid_up_to is a valid UTF-8 boundary within `combined`.
418            let text = unsafe { std::str::from_utf8_unchecked(&combined[..valid_up_to]) };
419            for ch in text.chars() {
420                if ch == '\n' {
421                    let line = std::mem::take(&mut line_buf);
422                    self.process_sse_line(&line, &mut acc, &stream_tx)?;
423                } else if ch != '\r' {
424                    line_buf.push(ch);
425                }
426            }
427        }
428        Self::warn_incomplete_utf8_tail(&incomplete);
429        if !line_buf.is_empty() {
430            self.process_sse_line(&line_buf, &mut acc, &stream_tx)?;
431        }
432
433        // Normal end of stream: assemble the Completion from the accumulated
434        // state (shared with the Goal-382 partial-return paths above).
435        let finish_reason = std::mem::take(&mut acc.finish_reason);
436        Ok(Self::build_completion(acc, finish_reason))
437    }
438
439    /// Log (and discard) a trailing incomplete UTF-8 sequence at end of stream.
440    ///
441    /// `#[mutants::skip]`: this helper only emits a warning, so mutating the
442    /// `!is_empty` check is behavior-equivalent and untestable without
443    /// asserting on log output.
444    #[cfg_attr(test, mutants::skip)]
445    fn warn_incomplete_utf8_tail(incomplete: &[u8]) {
446        if !incomplete.is_empty() {
447            tracing::warn!(
448                target: "recursive::llm",
449                bytes = incomplete.len(),
450                "SSE stream ended with incomplete UTF-8 sequence; discarding tail bytes"
451            );
452        }
453    }
454
455    /// Assemble a [`Completion`] from the accumulated SSE state. Shared by
456    /// the normal end-of-stream path and the Goal-382 partial-return paths
457    /// (stream read error / user-initiated cancel mid-stream). The
458    /// provider-level `finish_reason` is mapped to the OpenAI-compatible
459    /// naming used across the runtime.
460    fn build_completion(acc: SseAccum, finish_reason: Option<String>) -> Completion {
461        // Build final TokenUsage from accumulated fields
462        let usage = if acc.input_tokens.is_some() || acc.output_tokens.is_some() {
463            let prompt = acc.input_tokens.unwrap_or(0);
464            let completion = acc.output_tokens.unwrap_or(0);
465            Some(TokenUsage {
466                reasoning_tokens: 0,
467                prompt_tokens: prompt,
468                completion_tokens: completion,
469                total_tokens: prompt.saturating_add(completion),
470                cache_hit_tokens: acc.cache_read.unwrap_or(0),
471                // Anthropic reports `input_tokens` (fresh, non-cached) and
472                // `cache_creation_input_tokens` separately from the cache
473                // read. Both are "misses" for hit-rate purposes (processed
474                // fresh, not served from cache), so fold them together to keep
475                // the invariant `hit + miss == total input` consistent with
476                // DeepSeek. See `TokenUsage` docs.
477                cache_miss_tokens: prompt.saturating_add(acc.cache_creation.unwrap_or(0)),
478                // Goal 273: not yet reported by Anthropic. Default 0.
479            })
480        } else {
481            None
482        };
483
484        // Convert streamed tool calls to final ToolCall objects
485        let final_tool_calls: Vec<ToolCall> = acc
486            .tool_calls
487            .into_iter()
488            .filter(|tc| !tc.id.is_empty())
489            .map(|tc| {
490                let args: Value = if tc.partial_json.trim().is_empty() {
491                    Value::Object(Default::default())
492                } else {
493                    serde_json::from_str(&tc.partial_json)
494                        .unwrap_or_else(|_| Value::String(tc.partial_json.clone()))
495                };
496                ToolCall {
497                    id: tc.id,
498                    name: tc.name,
499                    arguments: args,
500                }
501            })
502            .collect();
503
504        Completion {
505            content: acc.content,
506            tool_calls: final_tool_calls,
507            finish_reason: finish_reason.map(|r| match r.as_str() {
508                "end_turn" => "stop".to_string(),
509                "max_tokens" => "length".to_string(),
510                "tool_use" => "tool_calls".to_string(),
511                other => other.to_string(),
512            }),
513            usage,
514            reasoning_content: if acc.reasoning_content.trim().is_empty() {
515                None
516            } else {
517                Some(acc.reasoning_content)
518            },
519        }
520    }
521
522    /// Process a single SSE line, mutating `acc` and forwarding any text /
523    /// reasoning delta to `stream_tx` immediately.
524    ///
525    /// Anthropic frames each event as an `event: <name>` line followed by a
526    /// `data: <json>` line; `acc.current_event` carries the name across the
527    /// two calls. Blank lines (event separators) match no prefix and are
528    /// no-ops.
529    fn process_sse_line(
530        &self,
531        line: &str,
532        acc: &mut SseAccum,
533        stream_tx: &Option<StreamSender>,
534    ) -> Result<()> {
535        if let Some(event_name) = line.strip_prefix("event: ") {
536            acc.current_event = Some(event_name.to_string());
537            return Ok(());
538        }
539        let Some(data) = line.strip_prefix("data: ") else {
540            return Ok(());
541        };
542        let event_type = acc.current_event.as_deref().unwrap_or("unknown");
543
544        match event_type {
545            "message_start" => {
546                let parsed: Value = serde_json::from_str(data).map_err(|e| {
547                    self.make_err(format!(
548                        "SSE parse error (message_start): {e}; data: {data}"
549                    ))
550                })?;
551                if let Some(msg) = parsed.get("message") {
552                    if let Some(u) = msg.get("usage") {
553                        acc.input_tokens = u
554                            .get("input_tokens")
555                            .and_then(|v| v.as_u64())
556                            .map(|v| v as u32);
557                        acc.cache_creation = u
558                            .get("cache_creation_input_tokens")
559                            .and_then(|v| v.as_u64())
560                            .map(|v| v as u32);
561                        acc.cache_read = u
562                            .get("cache_read_input_tokens")
563                            .and_then(|v| v.as_u64())
564                            .map(|v| v as u32);
565                    }
566                }
567            }
568            "content_block_start" => {
569                let parsed: Value = serde_json::from_str(data).map_err(|e| {
570                    self.make_err(format!(
571                        "SSE parse error (content_block_start): {e}; data: {data}"
572                    ))
573                })?;
574                if let Some(block) = parsed.get("content_block") {
575                    let block_type = block.get("type").and_then(|v| v.as_str()).unwrap_or("");
576                    let index = parsed.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
577                    match block_type {
578                        "tool_use" => {
579                            let id = block
580                                .get("id")
581                                .and_then(|v| v.as_str())
582                                .unwrap_or("")
583                                .to_string();
584                            let name = block
585                                .get("name")
586                                .and_then(|v| v.as_str())
587                                .unwrap_or("")
588                                .to_string();
589                            while acc.tool_calls.len() <= index {
590                                acc.tool_calls.push(StreamToolCall::default());
591                            }
592                            acc.tool_calls[index].id = id;
593                            acc.tool_calls[index].name = name;
594                        }
595                        "text" => {
596                            if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
597                                if !text.is_empty() {
598                                    acc.content.push_str(text);
599                                    if let Some(tx) = stream_tx {
600                                        let _ = tx.send(StreamChunk::Text(text.to_string()));
601                                    }
602                                }
603                            }
604                        }
605                        // Extended-thinking block (Anthropic, DeepSeek
606                        // `deepseek-v4-flash`, etc.). The opening block may
607                        // carry an initial `thinking` chunk.
608                        "thinking" => {
609                            if let Some(t) = block.get("thinking").and_then(|v| v.as_str()) {
610                                if !t.is_empty() {
611                                    acc.reasoning_content.push_str(t);
612                                    if let Some(tx) = stream_tx {
613                                        let _ = tx.send(StreamChunk::Reasoning(t.to_string()));
614                                    }
615                                }
616                            }
617                        }
618                        _ => {}
619                    }
620                }
621            }
622            "content_block_delta" => {
623                let parsed: Value = serde_json::from_str(data).map_err(|e| {
624                    self.make_err(format!(
625                        "SSE parse error (content_block_delta): {e}; data: {data}"
626                    ))
627                })?;
628                if let Some(delta) = parsed.get("delta") {
629                    let delta_type = delta.get("type").and_then(|v| v.as_str()).unwrap_or("");
630                    match delta_type {
631                        "text_delta" => Self::handle_text_delta(acc, delta, stream_tx),
632                        "input_json_delta" => {
633                            Self::handle_input_json_delta(acc, delta, &parsed);
634                        }
635                        // Extended-thinking streaming delta. Accumulate into
636                        // `reasoning_content` and forward as a `Reasoning`
637                        // chunk so the UI renders the thinking live, above
638                        // the answer.
639                        "thinking_delta" => Self::handle_thinking_delta(acc, delta, stream_tx),
640                        _ => {}
641                    }
642                }
643            }
644            "message_delta" => {
645                let parsed: Value = serde_json::from_str(data).map_err(|e| {
646                    self.make_err(format!(
647                        "SSE parse error (message_delta): {e}; data: {data}"
648                    ))
649                })?;
650                if let Some(delta) = parsed.get("delta") {
651                    if let Some(reason) = delta.get("stop_reason").and_then(|v| v.as_str()) {
652                        acc.finish_reason = Some(reason.to_string());
653                    }
654                }
655                if let Some(u) = parsed.get("usage") {
656                    acc.output_tokens = u
657                        .get("output_tokens")
658                        .and_then(|v| v.as_u64())
659                        .map(|v| v as u32);
660                    // 官方 Anthropic 把输入侧 usage 全放在 `message_start`,
661                    // `message_delta.usage` 只有 `output_tokens`。但部分
662                    // Anthropic 兼容面(实测 GLM:message_start 报
663                    // input_tokens=0,真实输入在 message_delta)在 delta 帧
664                    // 补报输入侧数字——出现即覆盖 message_start 的初值;
665                    // 缺键时保持官方语义不变。
666                    if let Some(v) = u.get("input_tokens").and_then(|v| v.as_u64()) {
667                        acc.input_tokens = Some(v as u32);
668                    }
669                    if let Some(v) = u.get("cache_read_input_tokens").and_then(|v| v.as_u64()) {
670                        acc.cache_read = Some(v as u32);
671                    }
672                    if let Some(v) = u
673                        .get("cache_creation_input_tokens")
674                        .and_then(|v| v.as_u64())
675                    {
676                        acc.cache_creation = Some(v as u32);
677                    }
678                }
679            }
680            // `message_stop` and `ping` are no-op frames (end-of-stream marker
681            // and keep-alive respectively); they intentionally fall through to
682            // the `_` arm. Keeping them as explicit empty arms would create
683            // dead "delete match arm" mutants with no behavioral difference.
684            _ => {
685                tracing::debug!(target: "recursive::llm", event = %event_type, data = %data, "unhandled SSE event");
686            }
687        }
688
689        acc.current_event = None;
690        Ok(())
691    }
692
693    /// Append a `text_delta` chunk to the content buffer and optionally
694    /// forward it to the live-streaming channel.
695    fn handle_text_delta(acc: &mut SseAccum, delta: &Value, stream_tx: &Option<StreamSender>) {
696        if let Some(text) = delta.get("text").and_then(|v| v.as_str()) {
697            if !text.is_empty() {
698                acc.content.push_str(text);
699                if let Some(tx) = stream_tx {
700                    let _ = tx.send(StreamChunk::Text(text.to_string()));
701                }
702            }
703        }
704    }
705
706    /// Append a partial JSON fragment for the tool call at `parsed["index"]`.
707    fn handle_input_json_delta(acc: &mut SseAccum, delta: &Value, parsed: &Value) {
708        if let Some(partial) = delta.get("partial_json").and_then(|v| v.as_str()) {
709            let index = parsed.get("index").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
710            while acc.tool_calls.len() <= index {
711                acc.tool_calls.push(StreamToolCall::default());
712            }
713            acc.tool_calls[index].partial_json.push_str(partial);
714        }
715    }
716
717    /// Append an extended-thinking chunk and optionally stream it live.
718    fn handle_thinking_delta(acc: &mut SseAccum, delta: &Value, stream_tx: &Option<StreamSender>) {
719        if let Some(t) = delta.get("thinking").and_then(|v| v.as_str()) {
720            if !t.is_empty() {
721                acc.reasoning_content.push_str(t);
722                if let Some(tx) = stream_tx {
723                    let _ = tx.send(StreamChunk::Reasoning(t.to_string()));
724                }
725            }
726        }
727    }
728}
729
730/// Mutable accumulator threaded through [`AnthropicProvider::process_sse_line`]
731/// while consuming a streamed Messages-API response.
732#[derive(Default)]
733struct SseAccum {
734    content: String,
735    reasoning_content: String,
736    tool_calls: Vec<StreamToolCall>,
737    finish_reason: Option<String>,
738    input_tokens: Option<u32>,
739    output_tokens: Option<u32>,
740    cache_creation: Option<u32>,
741    cache_read: Option<u32>,
742    /// Name from the most recent `event:` line, consumed by the next
743    /// `data:` line.
744    current_event: Option<String>,
745}
746
747/// Extract system message if present, return (system_content, remaining_messages).
748fn extract_system_message(messages: &[Message]) -> (Option<String>, Vec<Message>) {
749    if messages.first().is_some_and(|m| m.role == Role::System) {
750        let system = messages[0].content.clone();
751        let rest = messages[1..].to_vec();
752        (Some(system), rest)
753    } else {
754        (None, messages.to_vec())
755    }
756}
757
758/// Filter out any leading assistant messages that would cause Anthropic to reject the request.
759/// Anthropic requires the first message to be user, or system+user.
760fn filter_leading_assistant(messages: &[Message]) -> Vec<Message> {
761    let mut result = messages.to_vec();
762    while result
763        .first()
764        .is_some_and(|m| m.role == Role::Assistant && m.tool_calls.is_empty())
765    {
766        // Remove leading assistant message with no tool calls
767        result.remove(0);
768    }
769    result
770}
771
772fn build_request(
773    model: &str,
774    temperature: f64,
775    max_tokens: u32,
776    system: Option<&str>,
777    messages: &[Message],
778    tools: &[ToolSpec],
779) -> Value {
780    let mut req = serde_json::json!({
781        "model": model,
782        "max_tokens": max_tokens,
783        "temperature": temperature,
784    });
785
786    if let Some(sys) = system {
787        req["system"] = Value::String(sys.to_string());
788    }
789
790    let msgs: Vec<Value> = serialize_messages_anthropic(messages);
791    req["messages"] = Value::Array(msgs);
792
793    if !tools.is_empty() {
794        let tools_json: Vec<Value> = tools
795            .iter()
796            .map(|t| {
797                serde_json::json!({
798                    "name": t.name,
799                    "description": t.description,
800                    "input_schema": t.parameters,
801                })
802            })
803            .collect();
804        req["tools"] = Value::Array(tools_json);
805    }
806
807    req
808}
809
810fn serialize_message(m: &Message) -> Value {
811    let role = match m.role {
812        Role::System => "system",
813        Role::User => "user",
814        Role::Assistant => "assistant",
815        Role::Tool => "user", // Anthropic uses "user" for tool results
816    };
817
818    // If this message has tool calls, format as tool_use blocks
819    if !m.tool_calls.is_empty() {
820        let tool_uses: Vec<Value> = m
821            .tool_calls
822            .iter()
823            .map(|c| {
824                serde_json::json!({
825                    "type": "tool_use",
826                    "id": c.id,
827                    "name": c.name,
828                    "input": c.arguments,
829                })
830            })
831            .collect();
832        serde_json::json!({
833            "role": role,
834            "content": tool_uses,
835        })
836    } else if let Some(id) = &m.tool_call_id {
837        // Tool result message - format as tool_result content block
838        serde_json::json!({
839            "role": role,
840            "content": [{
841                "type": "tool_result",
842                "tool_use_id": id,
843                "content": m.content,
844            }],
845        })
846    } else {
847        // Regular text message
848        serde_json::json!({
849            "role": role,
850            "content": m.content,
851        })
852    }
853}
854
855/// Serialize a slice of messages for the Anthropic API, merging consecutive
856/// tool-result messages into a single user message with multiple `tool_result`
857/// content blocks.
858///
859/// Anthropic requires that when an assistant message contains multiple `tool_use`
860/// blocks, the immediately following user message must contain ALL corresponding
861/// `tool_result` blocks in a single message. Sending them as separate messages
862/// causes HTTP 400 "tool_use ids were found without tool_result blocks".
863///
864/// ToolSearch marker messages (tool_call_id set, content is a JSON array of
865/// resolved names) are serialized with `tool_reference` content blocks so the
866/// Anthropic API understands them as "here are the discovered tool schemas".
867fn serialize_messages_anthropic(messages: &[Message]) -> Vec<Value> {
868    let mut out: Vec<Value> = Vec::with_capacity(messages.len());
869    let mut i = 0;
870    while i < messages.len() {
871        let m = &messages[i];
872        // Check if this is a tool result message (Role::Tool or has tool_call_id)
873        if m.tool_call_id.is_some() || m.role == Role::Tool {
874            // Collect all consecutive tool result messages into one user message
875            let mut blocks: Vec<Value> = Vec::new();
876            while i < messages.len() {
877                let tm = &messages[i];
878                if tm.tool_call_id.is_none() && tm.role != Role::Tool {
879                    break;
880                }
881                if let Some(id) = &tm.tool_call_id {
882                    // ToolSearch markers store resolved names as a JSON array.
883                    // Serialize as tool_reference blocks so the API can expand
884                    // them into full schemas in the model's context.
885                    let content_value = if tm.content.starts_with('[') {
886                        if let Ok(names) = serde_json::from_str::<Vec<String>>(&tm.content) {
887                            Value::Array(
888                                names
889                                    .iter()
890                                    .map(|n| {
891                                        serde_json::json!({
892                                            "type": "tool_reference",
893                                            "tool_name": n,
894                                        })
895                                    })
896                                    .collect(),
897                            )
898                        } else {
899                            Value::String(tm.content.clone())
900                        }
901                    } else {
902                        Value::String(tm.content.clone())
903                    };
904                    blocks.push(serde_json::json!({
905                        "type": "tool_result",
906                        "tool_use_id": id,
907                        "content": content_value,
908                    }));
909                }
910                i += 1;
911            }
912            if !blocks.is_empty() {
913                out.push(serde_json::json!({
914                    "role": "user",
915                    "content": blocks,
916                }));
917            }
918        } else {
919            out.push(serialize_message(m));
920            i += 1;
921        }
922    }
923    out
924}
925
926#[derive(Debug, Deserialize)]
927struct AnthropicResponse {
928    #[serde(default)]
929    content: Vec<ContentBlock>,
930    #[serde(default)]
931    usage: Option<AnthropicUsage>,
932    #[serde(default)]
933    stop_reason: Option<String>,
934}
935
936#[derive(Debug, Deserialize)]
937#[serde(tag = "type")]
938enum ContentBlock {
939    #[serde(rename = "text")]
940    Text {
941        #[serde(default)]
942        text: String,
943    },
944    #[serde(rename = "tool_use")]
945    ToolUse {
946        id: String,
947        name: String,
948        #[serde(default)]
949        input: Value,
950    },
951    // Extended-thinking block (Anthropic extended thinking, MiniMax-M3,
952    // deepseek-v4-flash, etc.). The chain-of-thought text lives in the
953    // `thinking` field; `redacted_thinking` blocks carry no readable text
954    // and fall through to `Unknown`.
955    #[serde(rename = "thinking")]
956    Thinking {
957        #[serde(default)]
958        thinking: String,
959    },
960    #[serde(other)]
961    Unknown,
962}
963
964#[derive(Debug, Deserialize)]
965struct AnthropicUsage {
966    #[serde(default)]
967    input_tokens: Option<u32>,
968    #[serde(default)]
969    output_tokens: Option<u32>,
970    #[serde(default)]
971    cache_creation_input_tokens: Option<u32>,
972    #[serde(default)]
973    cache_read_input_tokens: Option<u32>,
974}
975
976impl AnthropicUsage {
977    fn to_token_usage(&self) -> TokenUsage {
978        let prompt = self.input_tokens.unwrap_or(0);
979        TokenUsage {
980            reasoning_tokens: 0,
981            prompt_tokens: prompt,
982            completion_tokens: self.output_tokens.unwrap_or(0),
983            total_tokens: prompt.saturating_add(self.output_tokens.unwrap_or(0)),
984            cache_hit_tokens: self.cache_read_input_tokens.unwrap_or(0),
985            // Fold fresh input + cache-creation into "miss" so that
986            // `hit + miss == total input`, matching DeepSeek's reporting.
987            // See `TokenUsage` docs.
988            cache_miss_tokens: prompt.saturating_add(self.cache_creation_input_tokens.unwrap_or(0)),
989            // Goal 273: Anthropic extended-thinking emits a separate
990            // `thinking_tokens` field. Default 0 — the field may be
991            // added once Anthropic's response shape is finalised.
992        }
993    }
994}
995
996/// Accumulator for tool calls being built from SSE stream events.
997#[derive(Default)]
998struct StreamToolCall {
999    id: String,
1000    name: String,
1001    partial_json: String,
1002}
1003
1004fn parse_completion(response: AnthropicResponse) -> Completion {
1005    let mut content = String::new();
1006    let mut reasoning_content = String::new();
1007    let mut tool_calls = Vec::new();
1008
1009    for block in response.content {
1010        match block {
1011            ContentBlock::Text { text } => {
1012                content.push_str(&text);
1013            }
1014            ContentBlock::Thinking { thinking } => {
1015                reasoning_content.push_str(&thinking);
1016            }
1017            ContentBlock::ToolUse { id, name, input } => {
1018                tool_calls.push(ToolCall {
1019                    id,
1020                    name,
1021                    arguments: input,
1022                });
1023            }
1024            ContentBlock::Unknown => {}
1025        }
1026    }
1027
1028    let finish_reason = response.stop_reason.map(|r| match r.as_str() {
1029        "end_turn" => "stop".to_string(),
1030        "max_tokens" => "length".to_string(),
1031        "tool_use" => "tool_calls".to_string(),
1032        other => other.to_string(),
1033    });
1034
1035    Completion {
1036        content,
1037        tool_calls,
1038        finish_reason,
1039        usage: response.usage.map(|u| u.to_token_usage()),
1040        reasoning_content: if reasoning_content.trim().is_empty() {
1041            None
1042        } else {
1043            Some(reasoning_content)
1044        },
1045    }
1046}
1047
1048#[cfg(test)]
1049mod tests {
1050    use super::*;
1051    use crate::tools::TOOL_SEARCH_TOOL_NAME;
1052    use serde_json::json;
1053
1054    #[test]
1055    fn extract_system_message_when_present() {
1056        let msgs = vec![
1057            Message::system("You are helpful.".to_string()),
1058            Message::user("Hello".to_string()),
1059        ];
1060        let (system, rest) = extract_system_message(&msgs);
1061        assert_eq!(system, Some("You are helpful.".to_string()));
1062        assert_eq!(rest.len(), 1);
1063        assert_eq!(rest[0].role, Role::User);
1064    }
1065
1066    #[test]
1067    fn extract_system_message_when_not_present() {
1068        let msgs = vec![Message::user("Hello".to_string())];
1069        let (system, rest) = extract_system_message(&msgs);
1070        assert_eq!(system, None);
1071        assert_eq!(rest.len(), 1);
1072    }
1073
1074    #[test]
1075    fn filter_leading_assistant_removes_empty_assistant() {
1076        let msgs = vec![
1077            Message::assistant("Hi".to_string()),
1078            Message::user("Hello".to_string()),
1079        ];
1080        let filtered = filter_leading_assistant(&msgs);
1081        assert_eq!(filtered.len(), 1);
1082        assert_eq!(filtered[0].role, Role::User);
1083    }
1084
1085    #[test]
1086    fn filter_leading_assistant_keeps_assistant_with_tool_calls() {
1087        let msgs = vec![
1088            Message::assistant_with_tool_calls(
1089                "I'll use a tool".to_string(),
1090                vec![ToolCall {
1091                    id: "call_1".to_string(),
1092                    name: "test".to_string(),
1093                    arguments: Value::Object(Default::default()),
1094                }],
1095            ),
1096            Message::user("Hello".to_string()),
1097        ];
1098        let filtered = filter_leading_assistant(&msgs);
1099        // Should keep the assistant message because it has tool_calls
1100        assert_eq!(filtered.len(), 2);
1101    }
1102
1103    #[test]
1104    fn parses_text_response() {
1105        let raw = r#"{
1106            "type": "message",
1107            "content": [{"type": "text", "text": "Hello"}],
1108            "stop_reason": "end_turn",
1109            "usage": {"input_tokens": 10, "output_tokens": 5}
1110        }"#;
1111        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1112        let c = parse_completion(parsed);
1113        assert_eq!(c.content, "Hello");
1114        assert!(c.tool_calls.is_empty());
1115        assert_eq!(c.finish_reason, Some("stop".to_string()));
1116        assert!(c.usage.is_some());
1117    }
1118
1119    #[test]
1120    fn parses_tool_use_response() {
1121        let raw = r#"{
1122            "type": "message",
1123            "content": [
1124                {"type": "tool_use", "id": "call_1", "name": "Read", "input": {"path": "src/lib.rs"}}
1125            ],
1126            "stop_reason": "tool_use",
1127            "usage": {"input_tokens": 100, "output_tokens": 50}
1128        }"#;
1129        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1130        let c = parse_completion(parsed);
1131        assert!(c.content.is_empty());
1132        assert_eq!(c.tool_calls.len(), 1);
1133        assert_eq!(c.tool_calls[0].id, "call_1");
1134        assert_eq!(c.tool_calls[0].name, "Read");
1135        assert_eq!(c.tool_calls[0].arguments["path"], "src/lib.rs");
1136    }
1137
1138    #[test]
1139    fn parses_mixed_content_response() {
1140        let raw = r#"{
1141            "type": "message",
1142            "content": [
1143                {"type": "text", "text": "I'll read that file. "},
1144                {"type": "tool_use", "id": "call_1", "name": "Read", "input": {"path": "src/lib.rs"}}
1145            ],
1146            "stop_reason": "tool_use",
1147            "usage": {"input_tokens": 50, "output_tokens": 25}
1148        }"#;
1149        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1150        let c = parse_completion(parsed);
1151        assert_eq!(c.content, "I'll read that file. ");
1152        assert_eq!(c.tool_calls.len(), 1);
1153    }
1154
1155    #[test]
1156    fn parses_thinking_block_into_reasoning_content() {
1157        // Anthropic extended thinking / DeepSeek deepseek-v4-flash emit a
1158        // `thinking` content block. It must land in `reasoning_content`,
1159        // not in the visible answer.
1160        let raw = r#"{
1161            "type": "message",
1162            "content": [
1163                {"type": "thinking", "thinking": "Let me reason about this.", "signature": "abc"},
1164                {"type": "text", "text": "The answer is 42."}
1165            ],
1166            "stop_reason": "end_turn",
1167            "usage": {"input_tokens": 10, "output_tokens": 5}
1168        }"#;
1169        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1170        let c = parse_completion(parsed);
1171        assert_eq!(c.content, "The answer is 42.");
1172        assert_eq!(
1173            c.reasoning_content.as_deref(),
1174            Some("Let me reason about this.")
1175        );
1176    }
1177
1178    #[test]
1179    fn parses_usage_correctly() {
1180        let raw = r#"{
1181            "type": "message",
1182            "content": [{"type": "text", "text": "hi"}],
1183            "stop_reason": "end_turn",
1184            "usage": {
1185                "input_tokens": 100,
1186                "output_tokens": 50,
1187                "cache_creation_input_tokens": 20,
1188                "cache_read_input_tokens": 30
1189            }
1190        }"#;
1191        let parsed: AnthropicResponse = serde_json::from_str(raw).unwrap();
1192        let c = parse_completion(parsed);
1193        let u = c.usage.unwrap();
1194        assert_eq!(u.prompt_tokens, 100);
1195        assert_eq!(u.completion_tokens, 50);
1196        // cache_read → hit.
1197        assert_eq!(u.cache_hit_tokens, 30);
1198        // Fresh input (100) + cache_creation (20) are folded into miss so
1199        // that hit + miss == total input tokens (30 + 130 == 100+30+20).
1200        assert_eq!(u.cache_miss_tokens, 120);
1201        assert_eq!(
1202            u.cache_hit_tokens + u.cache_miss_tokens,
1203            100 + 30 + 20,
1204            "hit + miss must equal total input tokens"
1205        );
1206    }
1207
1208    #[test]
1209    fn serializes_tool_result_message() {
1210        let msg = Message::tool_result("call_123".to_string(), "result content".to_string());
1211        let v = serialize_message(&msg);
1212        assert_eq!(v["role"], "user");
1213        let content = &v["content"][0];
1214        assert_eq!(content["type"], "tool_result");
1215        assert_eq!(content["tool_use_id"], "call_123");
1216        assert_eq!(content["content"], "result content");
1217    }
1218
1219    #[test]
1220    fn serializes_assistant_with_tool_calls() {
1221        let msg = Message::assistant_with_tool_calls(
1222            "I'll use a tool".to_string(),
1223            vec![ToolCall {
1224                id: "abc".to_string(),
1225                name: "Write".to_string(),
1226                arguments: serde_json::json!({"path": "a", "contents": "b"}),
1227            }],
1228        );
1229        let v = serialize_message(&msg);
1230        assert_eq!(v["role"], "assistant");
1231        let content = &v["content"][0];
1232        assert_eq!(content["type"], "tool_use");
1233        assert_eq!(content["name"], "Write");
1234    }
1235
1236    #[test]
1237    fn builds_request_with_system() {
1238        let req = build_request(
1239            "claude-3",
1240            0.2,
1241            4096,
1242            Some("You are helpful."),
1243            &[Message::user("Hi".to_string())],
1244            &[],
1245        );
1246        assert_eq!(req["system"], "You are helpful.");
1247        assert_eq!(req["model"], "claude-3");
1248    }
1249
1250    #[test]
1251    fn builds_request_without_system() {
1252        let req = build_request(
1253            "claude-3",
1254            0.2,
1255            4096,
1256            None,
1257            &[Message::user("Hi".to_string())],
1258            &[],
1259        );
1260        assert!(req.get("system").is_none());
1261    }
1262
1263    #[test]
1264    fn builds_request_with_tools() {
1265        let tools = vec![ToolSpec {
1266            name: "test".to_string(),
1267            description: "A test tool".to_string(),
1268            parameters: serde_json::json!({"type": "object"}),
1269        }];
1270        let req = build_request(
1271            "claude-3",
1272            0.2,
1273            4096,
1274            None,
1275            &[Message::user("Hi".to_string())],
1276            &tools,
1277        );
1278        assert!(req.get("tools").is_some());
1279    }
1280
1281    #[tokio::test]
1282    async fn test_a_mock_server_returns_canned_response() {
1283        // Spawn a mock server that returns a valid Anthropic response
1284        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1285        let addr = listener.local_addr().unwrap();
1286
1287        let handle = std::thread::spawn(move || {
1288            let (mut stream, _) = listener.accept().unwrap();
1289            use std::io::{Read, Write};
1290            let mut buf = [0u8; 4096];
1291            let _ = stream.read(&mut buf);
1292
1293            let body = r#"{"type":"message","content":[{"type":"text","text":"Hello from Anthropic"}],"stop_reason":"end_turn","usage":{"input_tokens":10,"output_tokens":5}}"#;
1294            let response = format!(
1295                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1296                body.len(),
1297                body
1298            );
1299            write!(stream, "{}", response).unwrap();
1300            stream.flush().unwrap();
1301        });
1302
1303        // Give the server a moment to start
1304        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1305
1306        let provider =
1307            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1308        let result = provider
1309            .complete(&[Message::user("hi".to_string())], &[])
1310            .await;
1311
1312        let _ = handle.join();
1313
1314        let completion = result.expect("should succeed");
1315        assert!(completion.content.contains("Hello from Anthropic"));
1316        assert!(completion.usage.is_some());
1317        let u = completion.usage.unwrap();
1318        assert_eq!(u.prompt_tokens, 10);
1319        assert_eq!(u.completion_tokens, 5);
1320    }
1321
1322    #[tokio::test]
1323    async fn test_b_error_response_includes_model_name() {
1324        // Spawn a mock server that returns 401
1325        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1326        let addr = listener.local_addr().unwrap();
1327
1328        let handle = std::thread::spawn(move || {
1329            let (mut stream, _) = listener.accept().unwrap();
1330            use std::io::{Read, Write};
1331            let mut buf = [0u8; 4096];
1332            let _ = stream.read(&mut buf);
1333
1334            let body = r#"{"error":{"type":"authentication_error","message":"Invalid API Key"}}"#;
1335            let response = format!(
1336                "HTTP/1.1 401 Unauthorized\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1337                body.len(),
1338                body
1339            );
1340            write!(stream, "{}", response).unwrap();
1341            stream.flush().unwrap();
1342        });
1343
1344        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1345
1346        let provider =
1347            AnthropicProvider::new(format!("http://{addr}"), "sk-invalid", "claude-3-opus")
1348                .unwrap();
1349        let err = provider
1350            .complete(&[Message::user("hi".to_string())], &[])
1351            .await
1352            .unwrap_err();
1353
1354        let _ = handle.join();
1355
1356        let msg = err.to_string();
1357        assert!(
1358            msg.contains("claude-3-opus"),
1359            "error should contain model name: {msg}"
1360        );
1361        assert!(
1362            msg.contains("401"),
1363            "error should contain HTTP status: {msg}"
1364        );
1365    }
1366
1367    #[tokio::test]
1368    async fn test_c_tool_use_extracts_correct_tool_call() {
1369        // Spawn a mock server that returns a tool_use response
1370        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1371        let addr = listener.local_addr().unwrap();
1372
1373        let handle = std::thread::spawn(move || {
1374            let (mut stream, _) = listener.accept().unwrap();
1375            use std::io::{Read, Write};
1376            let mut buf = [0u8; 4096];
1377            let _ = stream.read(&mut buf);
1378
1379            let body = r#"{"type":"message","content":[{"type":"tool_use","id":"call_abc123","name":"Read","input":{"path":"src/lib.rs"}}],"stop_reason":"tool_use","usage":{"input_tokens":50,"output_tokens":30}}"#;
1380            let response = format!(
1381                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1382                body.len(),
1383                body
1384            );
1385            write!(stream, "{}", response).unwrap();
1386            stream.flush().unwrap();
1387        });
1388
1389        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1390
1391        let provider =
1392            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1393        let result = provider
1394            .complete(&[Message::user("read the file".to_string())], &[])
1395            .await;
1396
1397        let _ = handle.join();
1398
1399        let completion = result.expect("should succeed");
1400        assert_eq!(completion.tool_calls.len(), 1);
1401        let tc = &completion.tool_calls[0];
1402        assert_eq!(tc.id, "call_abc123");
1403        assert_eq!(tc.name, "Read");
1404        assert_eq!(tc.arguments["path"], "src/lib.rs");
1405    }
1406
1407    #[tokio::test]
1408    async fn test_d_stream_request_includes_stream_true() {
1409        // Spawn a mock server that captures the request body and returns
1410        // a valid SSE response. Assert the request body contains "stream": true.
1411        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1412        let addr = listener.local_addr().unwrap();
1413        let captured = std::sync::Arc::new(std::sync::Mutex::new(String::new()));
1414        let captured_clone = captured.clone();
1415
1416        std::thread::spawn(move || {
1417            let (mut stream, _) = listener.accept().unwrap();
1418            use std::io::{Read, Write};
1419            let mut buf = [0u8; 8192];
1420            let n = stream.read(&mut buf).unwrap();
1421            let request = String::from_utf8_lossy(&buf[..n]).to_string();
1422            if let Some(body_start) = request.find("\r\n\r\n") {
1423                let body = request[body_start + 4..].trim().to_string();
1424                *captured_clone.lock().unwrap() = body.clone();
1425            }
1426            // Return a valid SSE stream (just message_stop to end quickly)
1427            let body = "event: message_stop\ndata: {\"type\":\"message_stop\"}\n\n";
1428            write!(
1429                stream,
1430                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1431                body.len(),
1432                body,
1433            ).unwrap();
1434            stream.flush().unwrap();
1435        });
1436
1437        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1438
1439        let provider =
1440            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1441        let _ = provider
1442            .stream(&[Message::user("hi".to_string())], &[], None, None)
1443            .await;
1444
1445        let body_str = captured.lock().unwrap().clone();
1446        let body: serde_json::Value = serde_json::from_str(&body_str).unwrap();
1447        assert_eq!(body["stream"], true);
1448    }
1449
1450    #[tokio::test]
1451    async fn test_e_stream_text_deltas_accumulate() {
1452        // Spawn a mock server that sends text deltas via SSE.
1453        // Assert the returned Completion's content is the concatenation.
1454        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1455        let addr = listener.local_addr().unwrap();
1456
1457        std::thread::spawn(move || {
1458            let (mut stream, _) = listener.accept().unwrap();
1459            use std::io::{Read, Write};
1460            let mut buf = [0u8; 4096];
1461            let _ = stream.read(&mut buf);
1462            let body = "\
1463event: message_start
1464data: {\"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}}}
1465
1466event: content_block_start
1467data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1468
1469event: content_block_delta
1470data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" \"}}
1471
1472event: content_block_delta
1473data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"World\"}}
1474
1475event: message_delta
1476data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1477
1478event: message_stop
1479data: {\"type\":\"message_stop\"}
1480
1481";
1482            write!(
1483                stream,
1484                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1485                body.len(),
1486                body,
1487            ).unwrap();
1488            stream.flush().unwrap();
1489        });
1490
1491        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1492
1493        let provider =
1494            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1495        let completion = provider
1496            .stream(&[Message::user("hi".to_string())], &[], None, None)
1497            .await
1498            .unwrap();
1499        assert_eq!(completion.content, "Hello World");
1500        assert_eq!(completion.finish_reason.as_deref(), Some("stop"));
1501        assert!(completion.usage.is_some());
1502        let u = completion.usage.unwrap();
1503        assert_eq!(u.prompt_tokens, 10);
1504        assert_eq!(u.completion_tokens, 5);
1505    }
1506
1507    #[tokio::test]
1508    async fn parse_sse_stream_returns_error_on_malformed_chunk() {
1509        // A garbled/truncated `data:` payload from a flaky proxy must
1510        // surface as a diagnosable `Error::Llm` whose message contains
1511        // "SSE parse error" (from `process_sse_line`'s per-event
1512        // `serde_json::from_str(...).map_err(...)`), NOT a silent panic or
1513        // an infinite hang. The malformed event comes FIRST so the parser
1514        // errors out before the terminal `message_stop` event.
1515        //
1516        // The `event:` line is load-bearing: a bare malformed `data:` line
1517        // with no preceding event name would fall into the "unhandled SSE
1518        // event" debug branch and be silently ignored — the test pins that
1519        // a malformed payload for a REAL event type is a hard error.
1520        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1521        let addr = listener.local_addr().unwrap();
1522
1523        std::thread::spawn(move || {
1524            let (mut stream, _) = listener.accept().unwrap();
1525            use std::io::{Read, Write};
1526            let mut buf = [0u8; 4096];
1527            let _ = stream.read(&mut buf);
1528            let body = "\
1529event: message_start
1530data: {not valid json
1531
1532event: message_stop
1533data: {\"type\":\"message_stop\"}
1534
1535";
1536            write!(
1537                stream,
1538                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1539                body.len(),
1540                body,
1541            ).unwrap();
1542            stream.flush().unwrap();
1543        });
1544
1545        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1546
1547        let provider =
1548            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1549        // Timeout guards against a parser bug that loops on malformed input
1550        // — the test should fail on timeout, not hang the suite.
1551        let result = tokio::time::timeout(
1552            std::time::Duration::from_secs(5),
1553            provider.stream(&[Message::user("hi".to_string())], &[], None, None),
1554        )
1555        .await
1556        .expect("SSE parser must not hang on malformed input");
1557
1558        let err = match result {
1559            Ok(_) => panic!("expected Err on malformed SSE chunk, got Ok"),
1560            Err(e) => e,
1561        };
1562        match err {
1563            Error::Llm { provider, message } => {
1564                assert!(
1565                    message.contains("SSE parse error"),
1566                    "error message should be diagnosable, got: {message}"
1567                );
1568                assert!(
1569                    provider.contains("claude-3-sonnet"),
1570                    "error should carry the provider/model name, got: {provider}"
1571                );
1572            }
1573            other => panic!("expected Error::Llm, got {other:?}"),
1574        }
1575    }
1576
1577    #[tokio::test]
1578    async fn parse_sse_stream_returns_partial_on_mid_stream_error() {
1579        // Goal 382: mirror of the openai.rs test — a network drop mid-stream
1580        // must NOT discard the text already accumulated in SseAccum. Serve a
1581        // couple of text deltas, then close the connection early with a
1582        // Content-Length LARGER than the body so the client's next
1583        // bytes_stream().next() sees a truncated-body read error (the
1584        // `Some(Err(e))` arm this goal touches).
1585        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1586        let addr = listener.local_addr().unwrap();
1587
1588        std::thread::spawn(move || {
1589            let (mut stream, _) = listener.accept().unwrap();
1590            use std::io::{Read, Write};
1591            let mut buf = [0u8; 4096];
1592            let _ = stream.read(&mut buf);
1593            let body = "\
1594event: message_start
1595data: {\"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}}}
1596
1597event: content_block_start
1598data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1599
1600event: content_block_delta
1601data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" partial\"}}
1602";
1603            write!(
1604                stream,
1605                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1606                9999,
1607                body,
1608            ).unwrap();
1609            stream.flush().unwrap();
1610            // Normal FIN close: the truncated body (9999 declared vs body.len()
1611            // sent) makes the client's next read error, deterministically.
1612        });
1613
1614        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1615
1616        let provider =
1617            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1618        let completion = provider
1619            .stream(&[Message::user("hi".to_string())], &[], None, None)
1620            .await
1621            .expect("partial content must be returned, not Err");
1622        assert_eq!(
1623            completion.content, "Hello partial",
1624            "accumulated deltas must survive the mid-stream read error",
1625        );
1626        assert_eq!(
1627            completion.finish_reason.as_deref(),
1628            Some("interrupted"),
1629            "partial completion must carry the interrupted sentinel",
1630        );
1631        assert!(
1632            completion.tool_calls.is_empty(),
1633            "half-received tool calls must be dropped from a partial completion",
1634        );
1635    }
1636
1637    #[tokio::test]
1638    async fn test_e2_stream_thinking_deltas_populate_reasoning() {
1639        // A streaming response with thinking_delta events must accumulate
1640        // into reasoning_content while text_delta stays in content.
1641        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1642        let addr = listener.local_addr().unwrap();
1643
1644        std::thread::spawn(move || {
1645            let (mut stream, _) = listener.accept().unwrap();
1646            use std::io::{Read, Write};
1647            let mut buf = [0u8; 4096];
1648            let _ = stream.read(&mut buf);
1649            let body = "\
1650event: message_start
1651data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"deepseek-v4-flash\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}
1652
1653event: content_block_start
1654data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}
1655
1656event: content_block_delta
1657data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Step one. \"}}
1658
1659event: content_block_delta
1660data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"Step two.\"}}
1661
1662event: content_block_start
1663data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}
1664
1665event: content_block_delta
1666data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"text_delta\",\"text\":\"Done.\"}}
1667
1668event: message_delta
1669data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1670
1671event: message_stop
1672data: {\"type\":\"message_stop\"}
1673
1674";
1675            write!(
1676                stream,
1677                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1678                body.len(),
1679                body,
1680            ).unwrap();
1681            stream.flush().unwrap();
1682        });
1683
1684        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1685
1686        let provider =
1687            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "deepseek-v4-flash")
1688                .unwrap();
1689        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1690        let completion = provider
1691            .stream(&[Message::user("hi".to_string())], &[], Some(tx), None)
1692            .await
1693            .unwrap();
1694
1695        assert_eq!(completion.content, "Done.");
1696        assert_eq!(
1697            completion.reasoning_content.as_deref(),
1698            Some("Step one. Step two.")
1699        );
1700
1701        // Both reasoning and answer chunks are forwarded live, reasoning
1702        // first (the model thinks before it speaks).
1703        let mut chunks = Vec::new();
1704        while let Ok(d) = rx.try_recv() {
1705            chunks.push(d);
1706        }
1707        assert_eq!(
1708            chunks,
1709            vec![
1710                StreamChunk::Reasoning("Step one. ".to_string()),
1711                StreamChunk::Reasoning("Step two.".to_string()),
1712                StreamChunk::Text("Done.".to_string()),
1713            ]
1714        );
1715    }
1716
1717    #[tokio::test]
1718    async fn test_f_stream_tool_use_assembles_tool_calls() {
1719        // Spawn a mock server that sends tool_use blocks via SSE.
1720        // Assert the returned Completion has the correct tool_calls.
1721        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1722        let addr = listener.local_addr().unwrap();
1723
1724        std::thread::spawn(move || {
1725            let (mut stream, _) = listener.accept().unwrap();
1726            use std::io::{Read, Write};
1727            let mut buf = [0u8; 4096];
1728            let _ = stream.read(&mut buf);
1729            let body = "\
1730event: message_start
1731data: {\"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}}}
1732
1733event: content_block_start
1734data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_abc123\",\"name\":\"Read\",\"input\":{}}}
1735
1736event: content_block_delta
1737data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"path\\\":\\\"src/\"}}
1738
1739event: content_block_delta
1740data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"lib.rs\\\"}\"}}
1741
1742event: message_delta
1743data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"output_tokens\":30}}
1744
1745event: message_stop
1746data: {\"type\":\"message_stop\"}
1747
1748";
1749            write!(
1750                stream,
1751                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1752                body.len(),
1753                body,
1754            ).unwrap();
1755            stream.flush().unwrap();
1756        });
1757
1758        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1759
1760        let provider =
1761            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1762        let completion = provider
1763            .stream(
1764                &[Message::user("read the file".to_string())],
1765                &[],
1766                None,
1767                None,
1768            )
1769            .await
1770            .unwrap();
1771        assert_eq!(completion.tool_calls.len(), 1);
1772        let tc = &completion.tool_calls[0];
1773        assert_eq!(tc.id, "call_abc123");
1774        assert_eq!(tc.name, "Read");
1775        assert_eq!(tc.arguments["path"], "src/lib.rs");
1776        assert_eq!(completion.finish_reason.as_deref(), Some("tool_calls"));
1777    }
1778
1779    #[tokio::test]
1780    async fn test_g_stream_tx_receives_text_chunks() {
1781        // Spawn a mock server that sends text deltas via SSE.
1782        // Assert the stream_tx channel receives incremental chunks.
1783        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1784        let addr = listener.local_addr().unwrap();
1785
1786        std::thread::spawn(move || {
1787            let (mut stream, _) = listener.accept().unwrap();
1788            use std::io::{Read, Write};
1789            let mut buf = [0u8; 4096];
1790            let _ = stream.read(&mut buf);
1791            let body = "\
1792event: message_start
1793data: {\"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}}}
1794
1795event: content_block_start
1796data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"A\"}}
1797
1798event: content_block_delta
1799data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"B\"}}
1800
1801event: content_block_delta
1802data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"C\"}}
1803
1804event: message_delta
1805data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":3}}
1806
1807event: message_stop
1808data: {\"type\":\"message_stop\"}
1809
1810";
1811            write!(
1812                stream,
1813                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1814                body.len(),
1815                body,
1816            ).unwrap();
1817            stream.flush().unwrap();
1818        });
1819
1820        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1821
1822        let provider =
1823            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1824        let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel();
1825        let completion = provider
1826            .stream(&[Message::user("hi".to_string())], &[], Some(tx), None)
1827            .await
1828            .unwrap();
1829        assert_eq!(completion.content, "ABC");
1830
1831        // Should have received 3 text deltas
1832        let mut deltas = Vec::new();
1833        while let Some(d) = rx.recv().await {
1834            deltas.push(d);
1835            if deltas.len() >= 3 {
1836                break;
1837            }
1838        }
1839        assert_eq!(
1840            deltas,
1841            vec![
1842                StreamChunk::Text("A".to_string()),
1843                StreamChunk::Text("B".to_string()),
1844                StreamChunk::Text("C".to_string()),
1845            ]
1846        );
1847    }
1848
1849    #[tokio::test]
1850    async fn test_h_stream_with_end_turn_produces_stop_reason() {
1851        // Spawn a mock server that sends a complete SSE stream with
1852        // end_turn stop_reason. Assert the Completion has finish_reason "stop".
1853        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
1854        let addr = listener.local_addr().unwrap();
1855
1856        std::thread::spawn(move || {
1857            let (mut stream, _) = listener.accept().unwrap();
1858            use std::io::{Read, Write};
1859            let mut buf = [0u8; 4096];
1860            let _ = stream.read(&mut buf);
1861            let body = "\
1862event: message_start
1863data: {\"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}}}
1864
1865event: content_block_start
1866data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
1867
1868event: message_delta
1869data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"output_tokens\":5}}
1870
1871event: message_stop
1872data: {\"type\":\"message_stop\"}
1873
1874";
1875            write!(
1876                stream,
1877                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
1878                body.len(),
1879                body,
1880            ).unwrap();
1881            stream.flush().unwrap();
1882        });
1883
1884        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
1885
1886        let provider =
1887            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
1888        let completion = provider
1889            .stream(&[Message::user("hi".to_string())], &[], None, None)
1890            .await
1891            .unwrap();
1892        assert_eq!(completion.content, "Hello");
1893        assert_eq!(completion.finish_reason.as_deref(), Some("stop"));
1894        assert!(completion.usage.is_some());
1895    }
1896
1897    #[test]
1898    fn policy_retries_5xx_with_exponential_backoff() {
1899        let policy = RetryPolicy::default();
1900        assert_eq!(
1901            policy.backoff_for(0, Some(503), false),
1902            Some(Duration::from_secs(1))
1903        );
1904        assert_eq!(
1905            policy.backoff_for(1, Some(500), false),
1906            Some(Duration::from_secs(2))
1907        );
1908        assert_eq!(policy.backoff_for(2, Some(500), false), None);
1909    }
1910
1911    #[test]
1912    fn policy_retries_network_errors() {
1913        let policy = RetryPolicy::default();
1914        assert_eq!(
1915            policy.backoff_for(0, None, true),
1916            Some(Duration::from_secs(1))
1917        );
1918    }
1919
1920    #[test]
1921    fn policy_does_not_retry_4xx() {
1922        let policy = RetryPolicy::default();
1923        assert_eq!(policy.backoff_for(0, Some(400), false), None);
1924        assert_eq!(policy.backoff_for(0, Some(401), false), None);
1925        assert_eq!(policy.backoff_for(0, Some(404), false), None);
1926    }
1927
1928    // ToolSearchTool is now a regular tool handled by run_core, not by the
1929    // provider. The provider only needs to:
1930    //   1. Serialize ToolSearch tool_result as tool_reference blocks.
1931    //   2. Send whatever specs run_core passes (already filtered to eager).
1932    // The following tests verify these two responsibilities.
1933
1934    #[test]
1935    fn build_request_serializes_passed_specs_verbatim() {
1936        let specs = vec![
1937            ToolSpec {
1938                name: "ToolSearchTool".to_string(),
1939                description: "search".to_string(),
1940                parameters: json!({"type": "object"}),
1941            },
1942            ToolSpec {
1943                name: "Read".to_string(),
1944                description: "Read a file".to_string(),
1945                parameters: json!({"type": "object"}),
1946            },
1947        ];
1948        let body = build_request("claude-3", 0.2, 4096, None, &[Message::user("hi")], &specs);
1949        let tools = body["tools"].as_array().expect("tools should be array");
1950        assert_eq!(tools.len(), 2);
1951        let names: Vec<&str> = tools.iter().map(|t| t["name"].as_str().unwrap()).collect();
1952        assert!(names.contains(&"ToolSearchTool"));
1953        assert!(names.contains(&"Read"));
1954        for t in tools {
1955            assert!(t["input_schema"].is_object());
1956        }
1957    }
1958
1959    #[test]
1960    fn serialize_messages_encodes_toolsearch_result_as_tool_references() {
1961        // When a ToolSearchTool result message has content that is a JSON
1962        // array of tool names, serialize_messages_anthropic must emit
1963        // tool_reference content blocks so Anthropic can expand them.
1964        let msgs = vec![
1965            Message::user("search for foo"),
1966            Message::assistant_with_tool_calls(
1967                String::new(),
1968                vec![ToolCall {
1969                    id: "call_xyz".to_string(),
1970                    name: TOOL_SEARCH_TOOL_NAME.to_string(),
1971                    arguments: json!({"query": "foo"}),
1972                }],
1973            ),
1974            Message {
1975                role: Role::User,
1976                content: r#"["notebook_edit"]"#.to_string(),
1977                tool_calls: Vec::new(),
1978                tool_call_id: Some("call_xyz".to_string()),
1979                reasoning_content: None,
1980                is_compaction_summary: false,
1981            },
1982        ];
1983
1984        let wire_msgs = serialize_messages_anthropic(&msgs);
1985        assert_eq!(wire_msgs.len(), 3);
1986        let last = &wire_msgs[2];
1987        assert_eq!(last["role"], "user");
1988        let content = last["content"].as_array().expect("content should be array");
1989        assert_eq!(content[0]["type"], "tool_result");
1990        assert_eq!(content[0]["tool_use_id"], "call_xyz");
1991        let inner = content[0]["content"]
1992            .as_array()
1993            .expect("inner content must be array");
1994        assert_eq!(inner.len(), 1);
1995        assert_eq!(inner[0]["type"], "tool_reference");
1996        assert_eq!(inner[0]["tool_name"], "notebook_edit");
1997    }
1998
1999    #[tokio::test]
2000    async fn complete_sends_specs_and_returns_tool_call() {
2001        // Verify the provider sends whatever specs it receives and parses
2002        // the response correctly. The deferred-tool filtering is done by
2003        // run_core before calling complete(), so the provider sees only
2004        // eager specs here.
2005        use std::io::{Read, Write};
2006
2007        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2008        let addr = listener.local_addr().unwrap();
2009
2010        std::thread::spawn(move || {
2011            let (mut stream, _) = listener.accept().unwrap();
2012            let mut buf = [0u8; 8192];
2013            let _ = stream.read(&mut buf);
2014            let response_body = r#"{"type":"message","content":[{"type":"tool_use","id":"call_1","name":"Read","input":{"path":"foo.txt"}}],"stop_reason":"tool_use","usage":{"input_tokens":50,"output_tokens":10}}"#;
2015            let response = format!(
2016                "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2017                response_body.len(),
2018                response_body,
2019            );
2020            stream.write_all(response.as_bytes()).unwrap();
2021        });
2022
2023        tokio::time::sleep(std::time::Duration::from_millis(50)).await;
2024        let provider =
2025            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "claude-3-sonnet").unwrap();
2026
2027        let specs = vec![
2028            ToolSpec {
2029                name: "ToolSearchTool".to_string(),
2030                description: "search for deferred tools".to_string(),
2031                parameters: json!({"type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"]}),
2032            },
2033            ToolSpec {
2034                name: "Read".to_string(),
2035                description: "Read a file".to_string(),
2036                parameters: json!({"type": "object"}),
2037            },
2038        ];
2039
2040        let result = provider
2041            .complete(&[Message::user("read foo.txt")], &specs)
2042            .await
2043            .expect("should succeed");
2044
2045        assert_eq!(result.tool_calls.len(), 1);
2046        assert_eq!(result.tool_calls[0].name, "Read");
2047    }
2048
2049    #[test]
2050    fn filter_leading_assistant_removes_multiple_empty_assistants() {
2051        // kills the `while` loop in filter_leading_assistant being replaced with `if`
2052        let msgs = vec![
2053            Message::assistant("first empty".to_string()),
2054            Message::assistant("second empty".to_string()),
2055            Message::user("Hello".to_string()),
2056        ];
2057        let filtered = filter_leading_assistant(&msgs);
2058        assert_eq!(
2059            filtered.len(),
2060            1,
2061            "both leading assistant messages must be removed"
2062        );
2063        assert_eq!(filtered[0].role, Role::User);
2064    }
2065
2066    #[test]
2067    fn filter_leading_assistant_noop_when_first_is_user() {
2068        // kills function-level replacement that always removes messages
2069        let msgs = vec![
2070            Message::user("Hello".to_string()),
2071            Message::assistant("Hi".to_string()),
2072        ];
2073        let filtered = filter_leading_assistant(&msgs);
2074        assert_eq!(
2075            filtered.len(),
2076            2,
2077            "must not remove messages when first is user"
2078        );
2079        assert_eq!(filtered[0].role, Role::User);
2080    }
2081
2082    #[test]
2083    fn serialize_message_plain_user_has_role_user_and_content() {
2084        // kills Role::User → "assistant" or "system" swap mutations
2085        let msg = Message::user("Hello world".to_string());
2086        let v = serialize_message(&msg);
2087        assert_eq!(v["role"].as_str(), Some("user"));
2088        assert_eq!(v["content"].as_str(), Some("Hello world"));
2089    }
2090
2091    #[test]
2092    fn handle_text_delta_appends_content_and_skips_empty() {
2093        let mut acc = SseAccum::default();
2094        let delta = json!({ "type": "text_delta", "text": "hello" });
2095        AnthropicProvider::handle_text_delta(&mut acc, &delta, &None);
2096        assert_eq!(acc.content, "hello");
2097
2098        let empty_delta = json!({ "type": "text_delta", "text": "" });
2099        AnthropicProvider::handle_text_delta(&mut acc, &empty_delta, &None);
2100        assert_eq!(acc.content, "hello", "empty text must not change content");
2101    }
2102
2103    #[test]
2104    fn handle_input_json_delta_fills_tool_call_slot() {
2105        let mut acc = SseAccum::default();
2106        let delta = json!({ "type": "input_json_delta", "partial_json": r#"{"pa"# });
2107        let parsed = json!({ "index": 0, "delta": delta });
2108        AnthropicProvider::handle_input_json_delta(&mut acc, &delta, &parsed);
2109        assert_eq!(acc.tool_calls.len(), 1);
2110        assert_eq!(acc.tool_calls[0].partial_json, r#"{"pa"#);
2111
2112        let delta2 = json!({ "type": "input_json_delta", "partial_json": r#"th":"x"}"# });
2113        let parsed2 = json!({ "index": 0, "delta": delta2 });
2114        AnthropicProvider::handle_input_json_delta(&mut acc, &delta2, &parsed2);
2115        assert_eq!(acc.tool_calls[0].partial_json, r#"{"path":"x"}"#);
2116    }
2117
2118    #[test]
2119    fn handle_thinking_delta_appends_reasoning_and_skips_empty() {
2120        let mut acc = SseAccum::default();
2121        let delta = json!({ "type": "thinking_delta", "thinking": "step 1" });
2122        AnthropicProvider::handle_thinking_delta(&mut acc, &delta, &None);
2123        assert_eq!(acc.reasoning_content, "step 1");
2124
2125        let empty = json!({ "type": "thinking_delta", "thinking": "" });
2126        AnthropicProvider::handle_thinking_delta(&mut acc, &empty, &None);
2127        assert_eq!(
2128            acc.reasoning_content, "step 1",
2129            "empty thinking must not change reasoning"
2130        );
2131    }
2132
2133    #[test]
2134    fn supports_deferred_tools_default_false_and_env_override() {
2135        // Kills `-> true` (177:9) and `== -> !=` (178:58) in
2136        // supports_deferred_tools: with a non-official base URL and no env
2137        // override the answer must be false; RECURSIVE_DEFERRED_TOOLS=1/true
2138        // must flip it to true.
2139        static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
2140        let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
2141        let provider = AnthropicProvider::new("http://third-party:9999", "sk-noop", "m").unwrap();
2142        let orig = std::env::var("RECURSIVE_DEFERRED_TOOLS").ok();
2143        std::env::remove_var("RECURSIVE_DEFERRED_TOOLS");
2144        assert!(
2145            !provider.supports_deferred_tools(),
2146            "non-official base URL + env unset must be false"
2147        );
2148        std::env::set_var("RECURSIVE_DEFERRED_TOOLS", "1");
2149        assert!(
2150            provider.supports_deferred_tools(),
2151            "env '1' must override to true"
2152        );
2153        std::env::set_var("RECURSIVE_DEFERRED_TOOLS", "true");
2154        assert!(
2155            provider.supports_deferred_tools(),
2156            "env 'true' must override to true"
2157        );
2158        match orig {
2159            Some(v) => std::env::set_var("RECURSIVE_DEFERRED_TOOLS", v),
2160            None => std::env::remove_var("RECURSIVE_DEFERRED_TOOLS"),
2161        }
2162    }
2163
2164    #[test]
2165    fn serialize_messages_keeps_user_message_after_tool_result() {
2166        // Kills `!= -> ==` at 852:57 in serialize_messages_anthropic: the
2167        // inner tool-result collection loop must BREAK on a following plain
2168        // user message; the `==` mutant would consume that user message
2169        // without serializing it (it has no tool_call_id to emit).
2170        let msgs = vec![
2171            Message::tool_result("call_1".to_string(), "done".to_string()),
2172            Message::user("next question".to_string()),
2173        ];
2174        let wire = serialize_messages_anthropic(&msgs);
2175        assert_eq!(
2176            wire.len(),
2177            2,
2178            "tool result AND following user message must both be serialized; got {wire:?}"
2179        );
2180        assert_eq!(wire[0]["role"], "user");
2181        assert_eq!(wire[1]["role"], "user");
2182        assert_eq!(wire[1]["content"], "next question");
2183    }
2184
2185    #[tokio::test]
2186    async fn post_with_retry_gives_up_after_max_transient_http_retries() {
2187        // Kills `attempt += 1 -> -= 1` (usize underflow panic on the first
2188        // retry) and `+= 1 -> *= 1` (attempt stuck at 0 → the loop retries
2189        // forever and hits the mutants test-timeout) at the transient-HTTP
2190        // retry site (line 140). A server that ALWAYS answers 503 makes the
2191        // real code give up after max_retries (backoff_for returns None).
2192        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2193        let addr = listener.local_addr().unwrap();
2194        std::thread::spawn(move || {
2195            use std::io::{Read, Write};
2196            loop {
2197                if let Ok((mut stream, _)) = listener.accept() {
2198                    let mut buf = [0u8; 4096];
2199                    let _ = stream.read(&mut buf);
2200                    let body = r#"{"error":"boom"}"#;
2201                    write!(
2202                        stream,
2203                        "HTTP/1.1 503 Service Unavailable\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2204                        body.len(),
2205                        body
2206                    )
2207                    .unwrap();
2208                    stream.flush().unwrap();
2209                }
2210            }
2211        });
2212        tokio::time::sleep(Duration::from_millis(50)).await;
2213
2214        let mut provider =
2215            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2216        provider.retry = RetryPolicy {
2217            max_retries: 2,
2218            initial_backoff: Duration::from_millis(5),
2219            max_backoff: Duration::from_millis(10),
2220        };
2221        let result = provider
2222            .post_with_retry(&format!("http://{addr}/v1/messages"), &json!({}))
2223            .await;
2224        assert!(
2225            result.is_err(),
2226            "must give up after max_retries transient HTTP failures"
2227        );
2228    }
2229
2230    #[tokio::test]
2231    async fn post_with_retry_gives_up_after_max_network_retries() {
2232        // Kills `attempt += 1` mutants at the network-error retry site
2233        // (line 155): server accepts then immediately drops the connection
2234        // 3 times → real code gives up with Err; `-=` panics on the first
2235        // retry and `*=` loops forever (test-timeout = caught).
2236        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2237        let addr = listener.local_addr().unwrap();
2238        std::thread::spawn(move || {
2239            for _ in 0..3 {
2240                if let Ok((stream, _)) = listener.accept() {
2241                    drop(stream);
2242                }
2243            }
2244        });
2245        tokio::time::sleep(Duration::from_millis(50)).await;
2246
2247        let mut provider =
2248            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2249        provider.retry = RetryPolicy {
2250            max_retries: 2,
2251            initial_backoff: Duration::from_millis(5),
2252            max_backoff: Duration::from_millis(10),
2253        };
2254        let result = provider
2255            .post_with_retry(&format!("http://{addr}/v1/messages"), &json!({}))
2256            .await;
2257        assert!(
2258            result.is_err(),
2259            "must give up after max_retries network failures"
2260        );
2261    }
2262
2263    #[tokio::test]
2264    async fn stream_inner_gives_up_after_max_transient_http_retries() {
2265        // Kills `attempt += 1` mutants at the stream_inner transient-HTTP
2266        // retry site (line 281): always-503 server → real code gives up
2267        // with Err after max_retries.
2268        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2269        let addr = listener.local_addr().unwrap();
2270        std::thread::spawn(move || {
2271            use std::io::{Read, Write};
2272            loop {
2273                if let Ok((mut stream, _)) = listener.accept() {
2274                    let mut buf = [0u8; 4096];
2275                    let _ = stream.read(&mut buf);
2276                    let body = r#"{"error":"boom"}"#;
2277                    write!(
2278                        stream,
2279                        "HTTP/1.1 503 Service Unavailable\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2280                        body.len(),
2281                        body
2282                    )
2283                    .unwrap();
2284                    stream.flush().unwrap();
2285                }
2286            }
2287        });
2288        tokio::time::sleep(Duration::from_millis(50)).await;
2289
2290        let mut provider =
2291            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2292        provider.retry = RetryPolicy {
2293            max_retries: 2,
2294            initial_backoff: Duration::from_millis(5),
2295            max_backoff: Duration::from_millis(10),
2296        };
2297        let result = provider
2298            .stream_inner(&[Message::user("hi".to_string())], &[], None, None)
2299            .await;
2300        assert!(
2301            result.is_err(),
2302            "stream_inner must give up after max_retries transient HTTP failures"
2303        );
2304    }
2305
2306    #[tokio::test]
2307    async fn stream_inner_gives_up_after_max_network_retries() {
2308        // Kills `attempt += 1` mutants at the stream_inner network-error
2309        // retry site (line 297): server accepts and drops 3 times.
2310        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2311        let addr = listener.local_addr().unwrap();
2312        std::thread::spawn(move || {
2313            for _ in 0..3 {
2314                if let Ok((stream, _)) = listener.accept() {
2315                    drop(stream);
2316                }
2317            }
2318        });
2319        tokio::time::sleep(Duration::from_millis(50)).await;
2320
2321        let mut provider =
2322            AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2323        provider.retry = RetryPolicy {
2324            max_retries: 2,
2325            initial_backoff: Duration::from_millis(5),
2326            max_backoff: Duration::from_millis(10),
2327        };
2328        let result = provider
2329            .stream_inner(&[Message::user("hi".to_string())], &[], None, None)
2330            .await;
2331        assert!(
2332            result.is_err(),
2333            "stream_inner must give up after max_retries network failures"
2334        );
2335    }
2336
2337    #[tokio::test]
2338    async fn parse_sse_stream_returns_partial_when_only_reasoning_accumulated() {
2339        // Kills the `have_partial` reasoning-condition mutants (delete `!` on
2340        // `!acc.reasoning_content.is_empty()`, `|| -> &&` at line 389) AND the
2341        // deleted `content_block_start "thinking"` arm (line 598): a stream
2342        // that carried ONLY a thinking block must still return a partial
2343        // Completion on a mid-stream read error (content empty, reasoning
2344        // non-empty).
2345        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2346        let addr = listener.local_addr().unwrap();
2347        std::thread::spawn(move || {
2348            let (mut stream, _) = listener.accept().unwrap();
2349            use std::io::{Read, Write};
2350            let mut buf = [0u8; 4096];
2351            let _ = stream.read(&mut buf);
2352            let body = "\
2353event: content_block_start
2354data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"Let me reason\"}}
2355";
2356            write!(
2357                stream,
2358                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2359                9999,
2360                body,
2361            )
2362            .unwrap();
2363            stream.flush().unwrap();
2364        });
2365        tokio::time::sleep(Duration::from_millis(50)).await;
2366
2367        let provider = AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2368        let completion = provider
2369            .stream(&[Message::user("hi".to_string())], &[], None, None)
2370            .await
2371            .expect("reasoning-only partial must be returned, not Err");
2372        assert_eq!(completion.finish_reason.as_deref(), Some("interrupted"));
2373        assert_eq!(completion.content, "", "no text content was streamed");
2374        assert_eq!(
2375            completion.reasoning_content.as_deref(),
2376            Some("Let me reason"),
2377            "accumulated thinking must survive the mid-stream error"
2378        );
2379    }
2380
2381    #[tokio::test]
2382    async fn parse_sse_stream_returns_partial_when_only_tool_call_started() {
2383        // Kills the `have_partial` tool-call-condition mutants (delete `!` on
2384        // `!acc.tool_calls.is_empty()`, `|| -> &&` at line 389): a stream that
2385        // started a tool_use block (but no text) must still return a partial
2386        // Completion on a mid-stream read error, with the half-received tool
2387        // call dropped (orphan risk).
2388        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2389        let addr = listener.local_addr().unwrap();
2390        std::thread::spawn(move || {
2391            let (mut stream, _) = listener.accept().unwrap();
2392            use std::io::{Read, Write};
2393            let mut buf = [0u8; 4096];
2394            let _ = stream.read(&mut buf);
2395            let body = "\
2396event: content_block_start
2397data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"call_1\",\"name\":\"Read\",\"input\":{}}}
2398";
2399            write!(
2400                stream,
2401                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2402                9999,
2403                body,
2404            )
2405            .unwrap();
2406            stream.flush().unwrap();
2407        });
2408        tokio::time::sleep(Duration::from_millis(50)).await;
2409
2410        let provider = AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2411        let completion = provider
2412            .stream(&[Message::user("hi".to_string())], &[], None, None)
2413            .await
2414            .expect("tool-call-only partial must be returned, not Err");
2415        assert_eq!(completion.finish_reason.as_deref(), Some("interrupted"));
2416        assert_eq!(completion.content, "", "no text content was streamed");
2417        assert!(
2418            completion.tool_calls.is_empty(),
2419            "half-received tool calls must be dropped from a partial completion"
2420        );
2421    }
2422
2423    #[tokio::test]
2424    async fn parse_sse_stream_processes_trailing_line_without_newline() {
2425        // Kills `delete !` on `if !line_buf.is_empty()` (line 435): a final
2426        // SSE `data:` line without a terminating newline must still be
2427        // processed at end of stream.
2428        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2429        let addr = listener.local_addr().unwrap();
2430        std::thread::spawn(move || {
2431            let (mut stream, _) = listener.accept().unwrap();
2432            use std::io::{Read, Write};
2433            let mut buf = [0u8; 4096];
2434            let _ = stream.read(&mut buf);
2435            // NOTE: the last delta line has NO trailing newline.
2436            let body = "\
2437event: content_block_start
2438data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"Hello\"}}
2439
2440event: content_block_delta
2441data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" world\"}}";
2442            write!(
2443                stream,
2444                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2445                body.len(),
2446                body,
2447            )
2448            .unwrap();
2449            stream.flush().unwrap();
2450        });
2451        tokio::time::sleep(Duration::from_millis(50)).await;
2452
2453        let provider = AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2454        let completion = provider
2455            .stream(&[Message::user("hi".to_string())], &[], None, None)
2456            .await
2457            .expect("stream should complete");
2458        assert_eq!(
2459            completion.content, "Hello world",
2460            "trailing line without newline must still be processed"
2461        );
2462    }
2463
2464    #[tokio::test]
2465    async fn build_completion_usage_from_input_tokens_only() {
2466        // Kills `|| -> &&` at line 452 in build_completion: usage must be
2467        // assembled when ONLY input_tokens arrived (message_start) and
2468        // output_tokens was never reported (no message_delta). The `&&`
2469        // mutant would drop the usage entirely.
2470        let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
2471        let addr = listener.local_addr().unwrap();
2472        std::thread::spawn(move || {
2473            let (mut stream, _) = listener.accept().unwrap();
2474            use std::io::{Read, Write};
2475            let mut buf = [0u8; 4096];
2476            let _ = stream.read(&mut buf);
2477            let body = "\
2478event: message_start
2479data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"m\",\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":7,\"output_tokens\":0}}}
2480
2481event: content_block_start
2482data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"hi\"}}
2483";
2484            write!(
2485                stream,
2486                "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
2487                body.len(),
2488                body,
2489            )
2490            .unwrap();
2491            stream.flush().unwrap();
2492        });
2493        tokio::time::sleep(Duration::from_millis(50)).await;
2494
2495        let provider = AnthropicProvider::new(format!("http://{addr}"), "sk-noop", "m").unwrap();
2496        let completion = provider
2497            .stream(&[Message::user("hi".to_string())], &[], None, None)
2498            .await
2499            .expect("stream should complete");
2500        let usage = completion
2501            .usage
2502            .expect("usage must be present when input_tokens was reported");
2503        assert_eq!(usage.prompt_tokens, 7);
2504    }
2505
2506    #[test]
2507    fn process_sse_line_thinking_block_start_populates_reasoning() {
2508        // Kills `delete match arm "thinking"` (line 598): a content_block_start
2509        // of type "thinking" carrying an initial thinking chunk must land in
2510        // `acc.reasoning_content`.
2511        let provider = AnthropicProvider::new("http://localhost:0", "sk-noop", "m").unwrap();
2512        let mut acc = SseAccum::default();
2513        provider
2514            .process_sse_line("event: content_block_start", &mut acc, &None)
2515            .unwrap();
2516        let line = r#"data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"initial thought"}}"#;
2517        provider.process_sse_line(line, &mut acc, &None).unwrap();
2518        assert_eq!(acc.reasoning_content, "initial thought");
2519    }
2520
2521    #[test]
2522    fn message_delta_usage_overrides_message_start_input() {
2523        // Anthropic 兼容面实测(GLM):message_start 报 input_tokens=0,真实
2524        // 输入在 message_delta.usage 里。delta 帧出现输入侧键时必须覆盖
2525        // message_start 的初值;官方 Anthropic 的 delta 帧无这些键,不受影响。
2526        let provider = AnthropicProvider::new("http://localhost:0", "sk-noop", "m").unwrap();
2527        let mut acc = SseAccum::default();
2528        provider
2529            .process_sse_line("event: message_start", &mut acc, &None)
2530            .unwrap();
2531        provider
2532            .process_sse_line(
2533                r#"data: {"type":"message_start","message":{"id":"m","type":"message","role":"assistant","content":[],"model":"GLM-5.2","usage":{"input_tokens":0,"output_tokens":0}}}"#,
2534                &mut acc,
2535                &None,
2536            )
2537            .unwrap();
2538        provider
2539            .process_sse_line("event: message_delta", &mut acc, &None)
2540            .unwrap();
2541        provider
2542            .process_sse_line(
2543                r#"data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":17,"output_tokens":3,"cache_read_input_tokens":7680,"cache_creation_input_tokens":36}}"#,
2544                &mut acc,
2545                &None,
2546            )
2547            .unwrap();
2548        let completion = AnthropicProvider::build_completion(acc, Some("end_turn".to_string()));
2549        let usage = completion.usage.expect("delta 帧报了 usage,必须存在");
2550        assert_eq!(usage.prompt_tokens, 17);
2551        assert_eq!(usage.completion_tokens, 3);
2552        assert_eq!(usage.cache_hit_tokens, 7680);
2553        // miss = fresh input + cache creation(见 TokenUsage 归一化文档)
2554        assert_eq!(usage.cache_miss_tokens, 17 + 36);
2555        assert_eq!(usage.total_tokens, 20);
2556    }
2557
2558    #[test]
2559    fn message_delta_without_input_keys_keeps_official_semantics() {
2560        // 官方 Anthropic:message_delta.usage 只有 output_tokens——message_start
2561        // 的输入侧数字必须原样保留(回归守卫)。
2562        let provider = AnthropicProvider::new("http://localhost:0", "sk-noop", "m").unwrap();
2563        let mut acc = SseAccum::default();
2564        provider
2565            .process_sse_line("event: message_start", &mut acc, &None)
2566            .unwrap();
2567        provider
2568            .process_sse_line(
2569                r#"data: {"type":"message_start","message":{"id":"m","type":"message","role":"assistant","content":[],"model":"claude-3","usage":{"input_tokens":50,"output_tokens":0,"cache_read_input_tokens":100}}}"#,
2570                &mut acc,
2571                &None,
2572            )
2573            .unwrap();
2574        provider
2575            .process_sse_line("event: message_delta", &mut acc, &None)
2576            .unwrap();
2577        provider
2578            .process_sse_line(
2579                r#"data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":7}}"#,
2580                &mut acc,
2581                &None,
2582            )
2583            .unwrap();
2584        let completion = AnthropicProvider::build_completion(acc, Some("end_turn".to_string()));
2585        let usage = completion.usage.expect("message_start 报了 usage");
2586        assert_eq!(usage.prompt_tokens, 50);
2587        assert_eq!(usage.completion_tokens, 7);
2588        assert_eq!(usage.cache_hit_tokens, 100);
2589    }
2590}