Skip to main content

myco_model/
anthropic.rs

1//! Anthropic Messages API backend.
2//!
3//! Ref: <https://platform.claude.com/docs/en/api/messages/create>
4//! Streaming: <https://platform.claude.com/docs/en/build-with-claude/streaming>
5//!
6//! Invariant: history thinking is never re-sent to the API; whether budget-mode
7//! thinking + tool use requires re-sending is an open question.
8
9use std::sync::Arc;
10
11use super::driver_core::{Slot, SlotMap, SseAccumulator};
12use super::*;
13
14/// Anthropic Messages API settings ([`BackendConfig::Anthropic`]).
15#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
16pub struct AnthropicBackendConfig {
17    pub anthropic_base_url: String,
18    pub anthropic_auth_token: String,
19    pub max_tokens_per_generate: usize,
20    pub debug_dump_api_requests: bool,
21    /// Maximum serialized request body, including history, images, tools, and prompts.
22    #[serde(default = "default_max_request_bytes")]
23    pub max_request_bytes: usize,
24    /// Transient-failure retry for this endpoint (`[gateways.NAME.retry]`).
25    #[serde(default)]
26    pub retry: RetryPolicy,
27    /// When set, enables Anthropic extended thinking at this effort level.
28    ///
29    /// The request shape follows the model's [`ThinkingMode`]: `adaptive` sends
30    /// `thinking.type: "adaptive"` plus `output_config.effort`; `budget` sends
31    /// `thinking.type: "enabled"` with a mapped `budget_tokens`; `none` sends
32    /// no thinking fields regardless of this value.
33    ///
34    /// Defaults to [`Effort::DEFAULT`] so thinking is always on for interactive use.
35    pub effort: Option<Effort>,
36}
37
38impl Default for AnthropicBackendConfig {
39    fn default() -> Self {
40        Self {
41            // No built-in gateway: the catalog (config.toml) supplies base_url.
42            anthropic_base_url: String::new(),
43            anthropic_auth_token: String::new(),
44            max_tokens_per_generate: 8192,
45            debug_dump_api_requests: false,
46            max_request_bytes: MAX_REQUEST_BYTES,
47            retry: RetryPolicy::default(),
48            effort: Some(Effort::DEFAULT),
49        }
50    }
51}
52
53/// Stateless Anthropic driver. Conversation history is owned by the caller.
54pub struct AnthropicGenerativeModel {
55    model: ModelSpec,
56    system_prompt: String,
57    tools: Vec<AnthropicTool>,
58    backend: AnthropicBackendConfig,
59    client: reqwest::Client,
60}
61
62impl AnthropicGenerativeModel {
63    pub fn new(
64        config: GenerativeModelConfig,
65        backend: AnthropicBackendConfig,
66    ) -> Result<Arc<Self>, ModelCreationError> {
67        // api.anthropic.com authenticates API keys (`sk-ant-…`) via the
68        // `x-api-key` header and rejects them as `Authorization: Bearer`;
69        // Bearer is the convention for gateway/OAuth tokens. Pick by token
70        // shape so both work against the default base URL.
71        let token = &backend.anthropic_auth_token;
72        let auth = if token.is_empty() {
73            None
74        } else if token.starts_with("sk-ant-") {
75            Some(("x-api-key", token.clone()))
76        } else {
77            Some(("authorization", format!("Bearer {token}")))
78        };
79        let client = driver_core::build_client(auth, &[("anthropic-version", "2023-06-01")])?;
80
81        let tools = config
82            .tools
83            .into_iter()
84            .map(|spec| AnthropicTool {
85                name: spec.name,
86                description: spec.description,
87                input_schema: spec.input_schema,
88            })
89            .collect();
90
91        Ok(Arc::new(Self {
92            model: config.model,
93            system_prompt: config.system_prompt,
94            tools,
95            backend,
96            client,
97        }))
98    }
99
100    /// Build (without sending) the streaming Messages request.
101    fn message_request(&self, messages: &[AnthropicMessage]) -> reqwest::RequestBuilder {
102        // Anthropic only honors `cache_control` on content blocks (system / messages /
103        // tools), never as a top-level request field. Put the breakpoint on the system
104        // prompt text block so the stable prefix can be cached across turns.
105        let system = if self.system_prompt.is_empty() {
106            None
107        } else {
108            Some(vec![AnthropicSystemText {
109                type_: "text",
110                text: &self.system_prompt,
111                cache_control: Some(AnthropicCacheControl::Ephemeral),
112            }])
113        };
114
115        let (thinking, output_config) =
116            thinking_request_fields(self.model.thinking, self.backend.effort);
117        // Anthropic requires max_tokens > thinking.budget_tokens for non-adaptive
118        // extended thinking (e.g. Haiku). Adaptive thinking has no budget field.
119        let mut max_tokens = self.backend.max_tokens_per_generate;
120        if let Some(AnthropicThinkingConfig::Enabled { budget_tokens }) = &thinking {
121            let need = (*budget_tokens as usize).saturating_add(1024);
122            if max_tokens <= *budget_tokens as usize {
123                max_tokens = need;
124            }
125        }
126        let request = AnthropicMessagesRequest {
127            max_tokens,
128            model: &self.model.api_id,
129            messages,
130            system,
131            tools: &self.tools,
132            stream: true,
133            thinking,
134            output_config,
135        };
136
137        self.client
138            .post(format!("{}/v1/messages", self.backend.anthropic_base_url))
139            .json(&request)
140    }
141}
142
143impl GenerativeModel for AnthropicGenerativeModel {
144    fn generate(&self, input: &[Message]) -> AsyncStream<GenerationEvent> {
145        if let Err(error) = validate_image_sizes(input, self.model.max_image_base64_bytes) {
146            return driver_core::error_stream(error);
147        }
148        let messages = match convert_messages(input) {
149            Ok(messages) => messages,
150            Err(e) => return driver_core::error_stream(e),
151        };
152        driver_core::spawn_generate(
153            self.message_request(&messages),
154            StreamAccumulator::default(),
155            "Anthropic",
156            self.backend.debug_dump_api_requests,
157            self.backend.max_request_bytes,
158        )
159    }
160}
161
162//
163// Message conversion
164//
165
166/// One role-alternating turn of Anthropic content — the merged form of one or
167/// more consecutive same-role [`Message`]s. A user turn may combine tool-result
168/// and text blocks, which no single `Message` variant can hold, so the merge
169/// yields these runs rather than `Message`s.
170struct MessageRun {
171    role: AnthropicRole,
172    content: Vec<AnthropicContent>,
173}
174
175/// Merge consecutive same-role turns into role-alternating runs. Anthropic
176/// requires alternating user/assistant roles, and tool-result blocks must lead
177/// the user turn they answer.
178///
179/// `wire_ids[i][j]` supplies the id for the `j`-th tool_use / tool_result of
180/// `input[i]` ([`wire_tool_ids`]); stored provider ids never reach the wire.
181fn merge_same_role_turns(input: &[Message], wire_ids: &[Vec<String>]) -> Box<[MessageRun]> {
182    let mut runs: Vec<MessageRun> = Vec::new();
183
184    for (i, message) in input.iter().enumerate() {
185        let (role, content): (_, Vec<AnthropicContent>) = match message {
186            Message::UserMessage { content } => (
187                AnthropicRole::User,
188                content.iter().cloned().filter_map(answer_block).collect(),
189            ),
190            Message::ToolResults { tool_use_results } => (
191                AnthropicRole::User,
192                tool_use_results
193                    .iter()
194                    .enumerate()
195                    .map(|(j, result)| AnthropicContent::ToolResult {
196                        tool_use_id: wire_ids[i][j].clone(),
197                        content: result
198                            .content
199                            .iter()
200                            .cloned()
201                            .filter_map(answer_block)
202                            .collect(),
203                        is_error: result.is_error,
204                        cache_control: None,
205                    })
206                    .collect(),
207            ),
208            Message::AssistantMessage {
209                content,
210                tool_uses,
211                turn_end_reason: _,
212            } => {
213                // Thinking may be stored in history for resume/UI; never echo it back to the API.
214                let mut blocks: Vec<AnthropicContent> =
215                    content.iter().cloned().filter_map(answer_block).collect();
216                for (j, tool_use) in tool_uses.iter().enumerate() {
217                    blocks.push(AnthropicContent::ToolUse {
218                        id: wire_ids[i][j].clone(),
219                        name: tool_use.name.clone(),
220                        input: tool_use.input.clone(),
221                        cache_control: None,
222                    });
223                }
224                // A thinking-only turn (e.g. max_tokens hit mid-thinking)
225                // strips to nothing; the API rejects empty assistant content
226                // on every later request, permanently wedging the session.
227                if blocks.is_empty() {
228                    continue;
229                }
230                (AnthropicRole::Assistant, blocks)
231            }
232        };
233
234        // Tool-result blocks must appear before any other content in a user turn.
235        if let Some(last) = runs.last_mut()
236            && last.role == role
237        {
238            if role == AnthropicRole::User {
239                let new_is_only_tool_results = !content.is_empty()
240                    && content
241                        .iter()
242                        .all(|c| matches!(c, AnthropicContent::ToolResult { .. }));
243                if new_is_only_tool_results {
244                    let mut combined = content;
245                    combined.append(&mut last.content);
246                    last.content = combined;
247                } else {
248                    last.content.extend(content);
249                }
250            } else {
251                last.content.extend(content);
252            }
253            continue;
254        }
255        runs.push(MessageRun { role, content });
256    }
257
258    runs.into_boxed_slice()
259}
260
261fn convert_messages(input: &[Message]) -> Result<Vec<AnthropicMessage>, GenerateError> {
262    // Merge into role-alternating runs, then emit one message per run — rolling
263    // cache breakpoints onto the final block of the last two. Marking a block
264    // caches the whole prefix up to it, and two breakpoints (rather than one)
265    // keep the previous turn's write inside Anthropic's 20-block lookback as the
266    // conversation grows — the recommended multi-turn pattern:
267    // <https://platform.claude.com/docs/en/build-with-claude/prompt-caching>
268    let wire_ids = wire_tool_ids(input)?;
269    let runs = merge_same_role_turns(input, &wire_ids);
270    let count = runs.len();
271    Ok(runs
272        .into_vec()
273        .into_iter()
274        .enumerate()
275        .map(|(i, MessageRun { role, mut content })| {
276            if i + 2 >= count
277                && let Some(last) = content.last_mut()
278            {
279                *last.cache_control_mut() = Some(AnthropicCacheControl::Ephemeral);
280            }
281            AnthropicMessage { role, content }
282        })
283        .collect())
284}
285
286//
287// Stream accumulation (the SSE drive loop is shared, in driver_core)
288//
289
290/// Maps Anthropic's unified content-block indices onto separate content/tool-use index spaces.
291#[derive(Default)]
292struct StreamAccumulator {
293    slots: SlotMap,
294    stop_reason: Option<AnthropicStopReason>,
295    finished: bool,
296}
297
298impl SseAccumulator for StreamAccumulator {
299    fn handle_data(&mut self, data: &str) -> Result<Vec<MessagePart>, GenerationFailure> {
300        let event: AnthropicStreamEvent = serde_json::from_str(data).map_err(|e| {
301            GenerateError::MalformedResponseError(format!(
302                "Failed to parse Anthropic SSE event JSON: {e}; data={data}"
303            ))
304        })?;
305        self.handle_event(event)
306    }
307
308    fn finished(&self) -> bool {
309        self.finished
310    }
311
312    fn finish(self) -> Result<(), GenerateError> {
313        driver_core::validate_finish("Anthropic", self.stop_reason.is_some(), std::iter::empty())
314    }
315}
316
317impl StreamAccumulator {
318    fn handle_event(
319        &mut self,
320        event: AnthropicStreamEvent,
321    ) -> Result<Vec<MessagePart>, GenerationFailure> {
322        let mut out = Vec::new();
323
324        match event {
325            AnthropicStreamEvent::MessageStart { message } => {
326                // Prompt-side counts (input + cache) arrive here; message_delta
327                // later carries only output_tokens. The accumulator merges both.
328                if let Some(u) = message.usage {
329                    out.push(MessagePart::Usage(u.into_token_usage()));
330                }
331            }
332            AnthropicStreamEvent::ContentBlockStart {
333                index,
334                content_block,
335            } => match content_block {
336                AnthropicStreamContentBlock::Text { text } => {
337                    let content_index = self.slots.open_content(index);
338                    out.push(MessagePart::ContentStart(ContentStart::Text {
339                        index: content_index,
340                    }));
341                    if !text.is_empty() {
342                        out.push(MessagePart::ContentDelta(ContentDelta::Text {
343                            index: content_index,
344                            delta: text,
345                        }));
346                    }
347                }
348                AnthropicStreamContentBlock::Thinking {
349                    thinking,
350                    signature,
351                } => {
352                    let content_index = self.slots.open_thinking(index);
353                    out.push(MessagePart::ContentStart(ContentStart::Thinking {
354                        index: content_index,
355                        signature,
356                        redacted: false,
357                    }));
358                    if !thinking.is_empty() {
359                        out.push(MessagePart::ContentDelta(ContentDelta::Thinking {
360                            index: content_index,
361                            delta: thinking,
362                        }));
363                    }
364                }
365                AnthropicStreamContentBlock::RedactedThinking { data } => {
366                    let content_index = self.slots.open_thinking(index);
367                    // Preserve opaque payload in signature; no plaintext deltas.
368                    out.push(MessagePart::ContentStart(ContentStart::Thinking {
369                        index: content_index,
370                        signature: if data.is_empty() { None } else { Some(data) },
371                        redacted: true,
372                    }));
373                }
374                AnthropicStreamContentBlock::ToolUse { name, input } => {
375                    // Input arrives via input_json_delta; starter object is usually empty.
376                    let _ = input;
377                    let tool_index = self.slots.open_tool_use(index);
378                    out.push(MessagePart::ToolUseStart(ToolUseStart {
379                        index: tool_index,
380                        name,
381                        thought_signature: None,
382                    }));
383                }
384                AnthropicStreamContentBlock::Other => {
385                    self.slots.ignore(index);
386                }
387            },
388            AnthropicStreamEvent::ContentBlockDelta { index, delta } => {
389                let slot = self.slots.get(index).ok_or_else(|| {
390                    GenerateError::MalformedResponseError(format!(
391                        "content_block_delta for unknown index {index}"
392                    ))
393                })?;
394
395                match (slot, delta) {
396                    (
397                        Slot::Content {
398                            index: content_index,
399                        }
400                        | Slot::Thinking {
401                            index: content_index,
402                        },
403                        AnthropicDelta::TextDelta { text },
404                    ) => {
405                        out.push(MessagePart::ContentDelta(ContentDelta::Text {
406                            index: content_index,
407                            delta: text,
408                        }));
409                    }
410                    (
411                        Slot::Content {
412                            index: content_index,
413                        }
414                        | Slot::Thinking {
415                            index: content_index,
416                        },
417                        AnthropicDelta::ThinkingDelta { thinking },
418                    ) => {
419                        out.push(MessagePart::ContentDelta(ContentDelta::Thinking {
420                            index: content_index,
421                            delta: thinking,
422                        }));
423                    }
424                    (
425                        Slot::Thinking {
426                            index: content_index,
427                        },
428                        AnthropicDelta::SignatureDelta { signature },
429                    ) => {
430                        out.push(MessagePart::ContentDelta(ContentDelta::Signature {
431                            index: content_index,
432                            delta: signature,
433                        }));
434                    }
435                    (
436                        Slot::Content {
437                            index: content_index,
438                        }
439                        | Slot::Thinking {
440                            index: content_index,
441                        },
442                        AnthropicDelta::InputJsonDelta { .. },
443                    ) => {
444                        return Err(GenerateError::MalformedResponseError(format!(
445                            "input_json_delta on content block index {content_index}"
446                        ))
447                        .into());
448                    }
449                    (
450                        Slot::ToolUse { index: tool_index },
451                        AnthropicDelta::InputJsonDelta { partial_json },
452                    ) => {
453                        out.push(MessagePart::ToolUseDelta(ToolUseDelta {
454                            index: tool_index,
455                            input_json_delta: partial_json,
456                        }));
457                    }
458                    (
459                        Slot::ToolUse { .. },
460                        AnthropicDelta::TextDelta { .. } | AnthropicDelta::ThinkingDelta { .. },
461                    ) => {
462                        return Err(GenerateError::MalformedResponseError(
463                            "text/thinking delta on tool_use block".into(),
464                        )
465                        .into());
466                    }
467                    (
468                        Slot::Content { .. } | Slot::ToolUse { .. },
469                        AnthropicDelta::SignatureDelta { .. },
470                    ) => {
471                        return Err(GenerateError::MalformedResponseError(
472                            "signature delta on a non-thinking block".into(),
473                        )
474                        .into());
475                    }
476                    (Slot::Ignored, _) | (_, AnthropicDelta::Other) => {}
477                }
478            }
479            AnthropicStreamEvent::ContentBlockStop => {}
480            AnthropicStreamEvent::MessageDelta { delta, usage } => {
481                if let Some(u) = usage {
482                    out.push(MessagePart::Usage(u.into_token_usage()));
483                }
484                if let Some(stop_reason) = delta.stop_reason {
485                    if matches!(stop_reason, AnthropicStopReason::Refusal) {
486                        return Err(GenerateError::RefusalError(
487                            "Anthropic stop_reason=refusal".into(),
488                        )
489                        .into());
490                    }
491                    self.stop_reason = Some(stop_reason.clone());
492                    out.push(MessagePart::TurnEndReason(TurnEndReason::from(stop_reason)));
493                }
494            }
495            AnthropicStreamEvent::MessageStop => {
496                self.finished = true;
497            }
498            AnthropicStreamEvent::Ping => {}
499            AnthropicStreamEvent::Error { error } => {
500                // The stream may report these failures after HTTP 200:
501                // https://platform.claude.com/docs/en/api/errors
502                return Err(GenerationFailure {
503                    retryable: matches!(
504                        error.get("type").and_then(serde_json::Value::as_str),
505                        Some(
506                            "overloaded_error" | "api_error" | "timeout_error" | "rate_limit_error"
507                        )
508                    ),
509                    cause: GenerateError::ExecutionError(format!(
510                        "Anthropic stream error event: {error}"
511                    )),
512                    retry_after: None,
513                });
514            }
515            AnthropicStreamEvent::Other => {}
516        }
517
518        Ok(out)
519    }
520}
521
522//
523// Anthropic streaming event types
524//
525
526#[derive(Debug, serde::Deserialize)]
527#[serde(tag = "type")]
528enum AnthropicStreamEvent {
529    #[serde(rename = "message_start")]
530    MessageStart {
531        #[serde(default)]
532        message: AnthropicStartMessage,
533    },
534    #[serde(rename = "content_block_start")]
535    ContentBlockStart {
536        index: usize,
537        content_block: AnthropicStreamContentBlock,
538    },
539    #[serde(rename = "content_block_delta")]
540    ContentBlockDelta { index: usize, delta: AnthropicDelta },
541    #[serde(rename = "content_block_stop")]
542    ContentBlockStop,
543    #[serde(rename = "message_delta")]
544    MessageDelta {
545        delta: AnthropicMessageDelta,
546        #[serde(default)]
547        usage: Option<AnthropicUsage>,
548    },
549    #[serde(rename = "message_stop")]
550    MessageStop,
551    #[serde(rename = "ping")]
552    Ping,
553    #[serde(rename = "error")]
554    Error { error: serde_json::Value },
555    #[serde(other)]
556    Other,
557}
558
559#[derive(Debug, serde::Deserialize)]
560#[serde(tag = "type")]
561enum AnthropicStreamContentBlock {
562    #[serde(rename = "text")]
563    Text {
564        #[serde(default)]
565        text: String,
566    },
567    #[serde(rename = "thinking")]
568    Thinking {
569        #[serde(default)]
570        thinking: String,
571        #[serde(default)]
572        signature: Option<String>,
573    },
574    #[serde(rename = "redacted_thinking")]
575    RedactedThinking {
576        #[serde(default)]
577        data: String,
578    },
579    #[serde(rename = "tool_use")]
580    ToolUse {
581        name: String,
582        #[serde(default)]
583        input: serde_json::Value,
584    },
585    #[serde(other)]
586    Other,
587}
588
589#[derive(Debug, serde::Deserialize)]
590#[serde(tag = "type")]
591enum AnthropicDelta {
592    #[serde(rename = "text_delta")]
593    TextDelta { text: String },
594    #[serde(rename = "thinking_delta")]
595    ThinkingDelta { thinking: String },
596    #[serde(rename = "input_json_delta")]
597    InputJsonDelta { partial_json: String },
598    #[serde(rename = "signature_delta")]
599    SignatureDelta { signature: String },
600    #[serde(other)]
601    Other,
602}
603
604#[derive(Debug, serde::Deserialize)]
605struct AnthropicMessageDelta {
606    stop_reason: Option<AnthropicStopReason>,
607}
608
609/// `message_start` payload; only its prompt-side `usage` is read.
610#[derive(Debug, Default, serde::Deserialize)]
611struct AnthropicStartMessage {
612    #[serde(default)]
613    usage: Option<AnthropicUsage>,
614}
615
616#[derive(Debug, Clone, serde::Deserialize)]
617struct AnthropicUsage {
618    #[serde(default)]
619    input_tokens: u64,
620    #[serde(default)]
621    output_tokens: u64,
622    #[serde(default)]
623    cache_read_input_tokens: Option<u64>,
624    #[serde(default)]
625    cache_creation_input_tokens: Option<u64>,
626}
627
628impl AnthropicUsage {
629    fn into_token_usage(self) -> crate::TokenUsage {
630        // Full prompt = input + cache read + cache write; cached_input = reads.
631        let cache_read = self.cache_read_input_tokens.unwrap_or(0);
632        let cache_creation = self.cache_creation_input_tokens.unwrap_or(0);
633        crate::TokenUsage {
634            input_tokens: self
635                .input_tokens
636                .saturating_add(cache_read)
637                .saturating_add(cache_creation),
638            output_tokens: self.output_tokens,
639            cached_input_tokens: cache_read,
640        }
641    }
642}
643
644//
645// Request / wire types
646//
647
648#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
649enum AnthropicRole {
650    #[serde(rename = "assistant")]
651    Assistant,
652    #[serde(rename = "user")]
653    User,
654}
655
656#[derive(Debug, Clone, serde::Serialize)]
657struct AnthropicMessage {
658    role: AnthropicRole,
659    content: Vec<AnthropicContent>,
660}
661
662/// One content block. Every kind carries an optional `cache_control` because
663/// Anthropic only accepts cache breakpoints on blocks — [`convert_messages`]
664/// sets it on the final block of a breakpoint message, and Anthropic then
665/// caches the whole prefix (tools + system + prior messages) up to and
666/// including that block.
667#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
668#[serde(tag = "type")]
669enum AnthropicContent {
670    #[serde(rename = "text")]
671    Text {
672        text: String,
673        #[serde(default, skip_serializing_if = "Option::is_none")]
674        cache_control: Option<AnthropicCacheControl>,
675    },
676
677    #[serde(rename = "image")]
678    Image {
679        source: AnthropicImageSource,
680        #[serde(default, skip_serializing_if = "Option::is_none")]
681        cache_control: Option<AnthropicCacheControl>,
682    },
683
684    #[serde(rename = "tool_use")]
685    ToolUse {
686        id: String,
687        name: String,
688        input: serde_json::Value,
689        #[serde(default, skip_serializing_if = "Option::is_none")]
690        cache_control: Option<AnthropicCacheControl>,
691    },
692
693    #[serde(rename = "tool_result")]
694    ToolResult {
695        tool_use_id: String,
696        content: Vec<AnthropicContent>,
697        is_error: bool,
698        #[serde(default, skip_serializing_if = "Option::is_none")]
699        cache_control: Option<AnthropicCacheControl>,
700    },
701}
702
703impl AnthropicContent {
704    /// The block's `cache_control` slot, whatever its kind.
705    fn cache_control_mut(&mut self) -> &mut Option<AnthropicCacheControl> {
706        match self {
707            AnthropicContent::Text { cache_control, .. }
708            | AnthropicContent::Image { cache_control, .. }
709            | AnthropicContent::ToolUse { cache_control, .. }
710            | AnthropicContent::ToolResult { cache_control, .. } => cache_control,
711        }
712    }
713}
714
715/// Wire form of Anthropic image `source`. Public `Content::Image.source` is an opaque
716/// string that we interpret as a URL, a `data:` URL, or raw base64 (default PNG).
717#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, PartialEq, Eq)]
718#[serde(tag = "type")]
719enum AnthropicImageSource {
720    #[serde(rename = "base64")]
721    Base64 { media_type: String, data: String },
722    #[serde(rename = "url")]
723    Url { url: String },
724}
725
726fn anthropic_image_source(source: String) -> AnthropicImageSource {
727    if source.starts_with("http://") || source.starts_with("https://") {
728        return AnthropicImageSource::Url { url: source };
729    }
730    if let Some(rest) = source.strip_prefix("data:") {
731        // data:[<media_type>][;base64],<data>
732        if let Some((meta, data)) = rest.split_once(',') {
733            let media_type = meta
734                .split(';')
735                .next()
736                .filter(|s| !s.is_empty())
737                .unwrap_or("image/png")
738                .to_string();
739            return AnthropicImageSource::Base64 {
740                media_type,
741                data: data.to_string(),
742            };
743        }
744    }
745    AnthropicImageSource::Base64 {
746        media_type: "image/png".into(),
747        data: source,
748    }
749}
750
751/// Wire block for one history content item. History thinking is never re-sent
752/// (module invariant), so `Content::Thinking` yields `None`.
753fn answer_block(content: Content) -> Option<AnthropicContent> {
754    match content {
755        Content::Text { text } | Content::System { text, .. } => Some(AnthropicContent::Text {
756            text,
757            cache_control: None,
758        }),
759        Content::Image { source } => Some(AnthropicContent::Image {
760            source: anthropic_image_source(source),
761            cache_control: None,
762        }),
763        Content::Thinking { .. } => None,
764    }
765}
766
767#[derive(Debug, serde::Serialize)]
768struct AnthropicMessagesRequest<'a> {
769    max_tokens: usize,
770    messages: &'a [AnthropicMessage],
771    model: &'a str,
772    #[serde(skip_serializing_if = "Option::is_none")]
773    system: Option<Vec<AnthropicSystemText<'a>>>,
774    tools: &'a [AnthropicTool],
775    stream: bool,
776    #[serde(skip_serializing_if = "Option::is_none")]
777    thinking: Option<AnthropicThinkingConfig>,
778    #[serde(skip_serializing_if = "Option::is_none")]
779    output_config: Option<AnthropicOutputConfig>,
780}
781
782/// Wire form of Anthropic `thinking` request field.
783///
784/// Newer models reject `type: "enabled"` and require adaptive thinking plus
785/// `output_config.effort`. Older models require `type: "enabled"` with a budget.
786#[derive(Debug, serde::Serialize, Clone, PartialEq, Eq)]
787#[serde(tag = "type")]
788enum AnthropicThinkingConfig {
789    #[serde(rename = "enabled")]
790    Enabled { budget_tokens: u32 },
791    #[serde(rename = "adaptive")]
792    Adaptive {
793        /// `"summarized"` surfaces readable thinking text; default on newest models is
794        /// `"omitted"` (empty `thinking` field).
795        #[serde(skip_serializing_if = "Option::is_none")]
796        display: Option<&'static str>,
797    },
798}
799
800#[derive(Debug, serde::Serialize, Clone, PartialEq, Eq)]
801struct AnthropicOutputConfig {
802    #[serde(skip_serializing_if = "Option::is_none")]
803    effort: Option<&'static str>,
804}
805
806/// Build `thinking` / `output_config` for the given model when effort is set.
807fn thinking_request_fields(
808    mode: ThinkingMode,
809    effort: Option<Effort>,
810) -> (
811    Option<AnthropicThinkingConfig>,
812    Option<AnthropicOutputConfig>,
813) {
814    let Some(effort) = effort else {
815        return (None, None);
816    };
817
818    match mode {
819        ThinkingMode::Adaptive => (
820            Some(AnthropicThinkingConfig::Adaptive {
821                // Agent UIs stream thinking; omit would yield empty thinking deltas.
822                display: Some("summarized"),
823            }),
824            Some(AnthropicOutputConfig {
825                effort: Some(effort.as_str()),
826            }),
827        ),
828        ThinkingMode::Budget => (
829            Some(AnthropicThinkingConfig::Enabled {
830                budget_tokens: effort.budget_tokens(),
831            }),
832            None,
833        ),
834        // `effort` is rejected for this protocol at catalog resolution.
835        ThinkingMode::Effort | ThinkingMode::None => (None, None),
836    }
837}
838
839/// System prompt as a content-block array so `cache_control` can be attached.
840#[derive(Debug, serde::Serialize)]
841struct AnthropicSystemText<'a> {
842    #[serde(rename = "type")]
843    type_: &'static str,
844    text: &'a str,
845    #[serde(skip_serializing_if = "Option::is_none")]
846    cache_control: Option<AnthropicCacheControl>,
847}
848
849#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Copy)]
850#[serde(tag = "type")]
851enum AnthropicCacheControl {
852    #[serde(rename = "ephemeral")]
853    Ephemeral,
854}
855
856#[derive(Debug, Clone, serde::Serialize)]
857struct AnthropicTool {
858    name: String,
859    description: String,
860    input_schema: serde_json::Value,
861}
862
863#[derive(Clone, Debug, serde::Deserialize)]
864enum AnthropicStopReason {
865    #[serde(rename = "end_turn")]
866    EndTurn,
867    #[serde(rename = "max_tokens")]
868    MaxTokens,
869    #[serde(rename = "stop_sequence")]
870    StopSequence,
871    #[serde(rename = "tool_use")]
872    ToolUse,
873    #[serde(rename = "pause_turn")]
874    PauseTurn,
875    #[serde(rename = "refusal")]
876    Refusal,
877    /// The API grows stop reasons over time (`model_context_window_exceeded`
878    /// arrived in 2025); an unknown one must not fail the whole message_delta
879    /// event and discard an already-streamed generation.
880    #[serde(other)]
881    Unknown,
882}
883
884impl From<AnthropicStopReason> for TurnEndReason {
885    fn from(stop_reason: AnthropicStopReason) -> Self {
886        match stop_reason {
887            AnthropicStopReason::EndTurn => TurnEndReason::EndTurn,
888            AnthropicStopReason::MaxTokens => TurnEndReason::MaxTokens,
889            AnthropicStopReason::ToolUse => TurnEndReason::ToolUse,
890            AnthropicStopReason::StopSequence => {
891                TurnEndReason::Other("Anthropic::StopSequence".into())
892            }
893            AnthropicStopReason::PauseTurn => TurnEndReason::Other("Anthropic::PauseTurn".into()),
894            AnthropicStopReason::Refusal => TurnEndReason::Other("Anthropic::Refusal".into()),
895            AnthropicStopReason::Unknown => TurnEndReason::Other("Anthropic::Unknown".into()),
896        }
897    }
898}
899
900//
901// Tests
902//
903
904#[cfg(test)]
905mod tests {
906    use super::*;
907    use crate::test_support::{
908        assistant, assistant_tool, expect_text_delta, expect_thinking_delta,
909        expect_tool_args_delta, expect_tool_start, tool_results, user,
910    };
911
912    fn is_legal_anthropic_id(id: &str) -> bool {
913        !id.is_empty()
914            && id
915                .chars()
916                .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
917    }
918
919    #[test]
920    fn system_parts_reach_the_provider_in_text_and_image_messages_without_metadata() {
921        for with_image in [false, true] {
922            let mut content = vec![Content::System {
923                kind: "private_kind".into(),
924                text: "runtime notice".into(),
925                data: serde_json::json!({"private_metadata":true}),
926            }];
927            if with_image {
928                content.push(Content::Image {
929                    source: "data:image/png;base64,AAAA".into(),
930                });
931            }
932            content.push(Content::Text {
933                text: "human request".into(),
934            });
935            let input = [Message::UserMessage { content }];
936            let wire = serde_json::to_string(&convert_messages(&input).unwrap()).unwrap();
937            assert!(wire.contains("runtime notice"), "{wire}");
938            assert!(wire.contains("human request"), "{wire}");
939            assert!(!wire.contains("private_kind"), "{wire}");
940            assert!(!wire.contains("private_metadata"), "{wire}");
941        }
942    }
943
944    #[test]
945    fn test_role_serdes() {
946        let role = AnthropicRole::Assistant;
947        let json = serde_json::to_string(&role).unwrap();
948        assert_eq!(json, r#""assistant""#);
949    }
950
951    #[test]
952    fn test_content_serdes() {
953        let content = AnthropicContent::Text {
954            text: "Hello, world".to_string(),
955            cache_control: None,
956        };
957        let json = serde_json::to_string(&content).unwrap();
958        assert_eq!(json, r#"{"type":"text","text":"Hello, world"}"#);
959    }
960
961    #[test]
962    fn cache_breakpoints_mark_last_two_messages() {
963        let input = [user("one"), assistant("two"), user("three")];
964        let json = serde_json::to_value(convert_messages(&input).unwrap()).unwrap();
965        // The final two messages carry a breakpoint on their last block.
966        assert_eq!(json[2]["content"][0]["cache_control"]["type"], "ephemeral");
967        assert_eq!(json[1]["content"][0]["cache_control"]["type"], "ephemeral");
968        // The oldest message does not.
969        assert!(json[0]["content"][0].get("cache_control").is_none());
970    }
971
972    #[test]
973    fn cache_breakpoint_marks_only_final_block_not_nested() {
974        let input = [
975            Message::AssistantMessage {
976                content: vec![],
977                tool_uses: vec![
978                    ToolUse {
979                        name: "x".into(),
980                        input: serde_json::Value::Null,
981                        thought_signature: None,
982                    },
983                    ToolUse {
984                        name: "x".into(),
985                        input: serde_json::Value::Null,
986                        thought_signature: None,
987                    },
988                ],
989                turn_end_reason: None,
990            },
991            Message::ToolResults {
992                tool_use_results: vec![
993                    ToolResult {
994                        content: vec![Content::Text { text: "ra".into() }],
995                        is_error: false,
996                        status: None,
997                        resource: None,
998                    },
999                    ToolResult {
1000                        content: vec![Content::Text { text: "rb".into() }],
1001                        is_error: false,
1002                        status: None,
1003                        resource: None,
1004                    },
1005                ],
1006            },
1007        ];
1008        let json = serde_json::to_value(convert_messages(&input).unwrap()).unwrap();
1009        // Last message has two tool_result blocks; only the final one is marked.
1010        assert_eq!(json[1]["content"][1]["cache_control"]["type"], "ephemeral");
1011        assert!(json[1]["content"][0].get("cache_control").is_none());
1012        // The breakpoint sits on the block, not spliced into its nested body.
1013        assert!(
1014            json[1]["content"][1]["content"][0]
1015                .get("cache_control")
1016                .is_none()
1017        );
1018    }
1019
1020    #[test]
1021    fn convert_messages_empty_is_noop() {
1022        assert!(convert_messages(&[]).unwrap().is_empty());
1023    }
1024
1025    /// An image inside a tool result (e.g. `view_image`) must reach the API as
1026    /// a nested image block, not be dropped or stringified.
1027    #[test]
1028    fn tool_result_image_serializes_as_nested_image_block() {
1029        let input = [
1030            assistant_tool(None, "view_image", serde_json::json!({})),
1031            Message::ToolResults {
1032                tool_use_results: vec![ToolResult {
1033                    content: vec![Content::Image {
1034                        source: "data:image/png;base64,AAAA".into(),
1035                    }],
1036                    is_error: false,
1037                    status: None,
1038                    resource: None,
1039                }],
1040            },
1041        ];
1042        let json = serde_json::to_value(convert_messages(&input).unwrap()).unwrap();
1043        assert_eq!(json[1]["role"], "user");
1044        assert_eq!(json[1]["content"][0]["type"], "tool_result");
1045        assert_eq!(json[1]["content"][0]["content"][0]["type"], "image");
1046        assert_eq!(
1047            json[1]["content"][0]["content"][0]["source"]["type"],
1048            "base64"
1049        );
1050        assert_eq!(
1051            json[1]["content"][0]["content"][0]["source"]["media_type"],
1052            "image/png"
1053        );
1054        assert_eq!(
1055            json[1]["content"][0]["content"][0]["source"]["data"],
1056            "AAAA"
1057        );
1058    }
1059
1060    #[test]
1061    fn image_source_url_and_base64_wire_format() {
1062        let url = answer_block(Content::Image {
1063            source: "https://example.com/a.png".into(),
1064        })
1065        .unwrap();
1066        let url_json = serde_json::to_value(&url).unwrap();
1067        assert_eq!(url_json["type"], "image");
1068        assert_eq!(url_json["source"]["type"], "url");
1069        assert_eq!(url_json["source"]["url"], "https://example.com/a.png");
1070
1071        let b64 = answer_block(Content::Image {
1072            source: "iVBORw0KGgo=".into(),
1073        })
1074        .unwrap();
1075        let b64_json = serde_json::to_value(&b64).unwrap();
1076        assert_eq!(b64_json["source"]["type"], "base64");
1077        assert_eq!(b64_json["source"]["media_type"], "image/png");
1078        assert_eq!(b64_json["source"]["data"], "iVBORw0KGgo=");
1079
1080        let data_url = answer_block(Content::Image {
1081            source: "data:image/jpeg;base64,/9j/4AAQ".into(),
1082        })
1083        .unwrap();
1084        let data_json = serde_json::to_value(&data_url).unwrap();
1085        assert_eq!(data_json["source"]["type"], "base64");
1086        assert_eq!(data_json["source"]["media_type"], "image/jpeg");
1087        assert_eq!(data_json["source"]["data"], "/9j/4AAQ");
1088    }
1089
1090    #[test]
1091    fn request_puts_cache_control_on_system_block_not_root() {
1092        let system = vec![AnthropicSystemText {
1093            type_: "text",
1094            text: "You are helpful.",
1095            cache_control: Some(AnthropicCacheControl::Ephemeral),
1096        }];
1097        let request = AnthropicMessagesRequest {
1098            max_tokens: 128,
1099            model: "claude-haiku-4-5",
1100            messages: &[],
1101            system: Some(system),
1102            tools: &[],
1103            stream: true,
1104            thinking: None,
1105            output_config: None,
1106        };
1107        let json = serde_json::to_value(&request).unwrap();
1108        assert!(json.get("cache_control").is_none());
1109        assert_eq!(json["system"][0]["type"], "text");
1110        assert_eq!(json["system"][0]["text"], "You are helpful.");
1111        assert_eq!(json["system"][0]["cache_control"]["type"], "ephemeral");
1112        assert!(json["system"][0]["cache_control"].get("ttl").is_none());
1113    }
1114
1115    #[test]
1116    fn request_omits_system_when_empty() {
1117        let request = AnthropicMessagesRequest {
1118            max_tokens: 128,
1119            model: "claude-haiku-4-5",
1120            messages: &[],
1121            system: None,
1122            tools: &[],
1123            stream: true,
1124            thinking: None,
1125            output_config: None,
1126        };
1127        let json = serde_json::to_value(&request).unwrap();
1128        assert!(json.get("system").is_none());
1129    }
1130
1131    #[test]
1132    fn adaptive_thinking_uses_effort_not_budget() {
1133        let (thinking, output_config) =
1134            thinking_request_fields(ThinkingMode::Adaptive, Some(Effort::High));
1135        let request = AnthropicMessagesRequest {
1136            max_tokens: 128,
1137            model: "claude-opus-4-8",
1138            messages: &[],
1139            system: None,
1140            tools: &[],
1141            stream: true,
1142            thinking,
1143            output_config,
1144        };
1145        let json = serde_json::to_value(&request).unwrap();
1146        assert_eq!(json["thinking"]["type"], "adaptive");
1147        assert_eq!(json["thinking"]["display"], "summarized");
1148        assert!(json["thinking"].get("budget_tokens").is_none());
1149        assert_eq!(json["output_config"]["effort"], "high");
1150    }
1151
1152    #[test]
1153    fn manual_thinking_uses_budget_tokens() {
1154        let (thinking, output_config) =
1155            thinking_request_fields(ThinkingMode::Budget, Some(Effort::Medium));
1156        let request = AnthropicMessagesRequest {
1157            max_tokens: 128,
1158            model: "claude-haiku-4-5",
1159            messages: &[],
1160            system: None,
1161            tools: &[],
1162            stream: true,
1163            thinking,
1164            output_config,
1165        };
1166        let json = serde_json::to_value(&request).unwrap();
1167        assert_eq!(json["thinking"]["type"], "enabled");
1168        assert_eq!(
1169            json["thinking"]["budget_tokens"],
1170            Effort::Medium.budget_tokens()
1171        );
1172        assert!(json.get("output_config").is_none());
1173    }
1174
1175    #[test]
1176    fn effort_budget_token_mapping() {
1177        assert_eq!(Effort::Low.budget_tokens(), 1_024);
1178        assert_eq!(Effort::Medium.budget_tokens(), 4_096);
1179        assert_eq!(Effort::High.budget_tokens(), 16_000);
1180        assert_eq!(Effort::Max.budget_tokens(), 64_000);
1181    }
1182
1183    #[test]
1184    fn max_tokens_raised_above_enabled_thinking_budget() {
1185        // Default backend max_tokens is 8192; High budget is 16000 — request builder
1186        // must raise max_tokens (validated by reimplementing the clamp here).
1187        let budget = Effort::High.budget_tokens() as usize;
1188        let configured = 8192usize;
1189        let max_tokens = if configured <= budget {
1190            budget.saturating_add(1024)
1191        } else {
1192            configured
1193        };
1194        assert!(max_tokens > budget);
1195        assert_eq!(max_tokens, 16_000 + 1024);
1196    }
1197
1198    #[test]
1199    fn thinking_omitted_when_effort_unset() {
1200        let (thinking, output_config) = thinking_request_fields(ThinkingMode::Adaptive, None);
1201        assert!(thinking.is_none());
1202        assert!(output_config.is_none());
1203    }
1204
1205    #[test]
1206    fn thinking_mode_none_sends_no_thinking_fields() {
1207        let (thinking, output_config) =
1208            thinking_request_fields(ThinkingMode::None, Some(Effort::High));
1209        assert!(thinking.is_none());
1210        assert!(output_config.is_none());
1211    }
1212
1213    #[test]
1214    fn test_convert_messages_merges_consecutive_user() {
1215        let input = [
1216            assistant_tool(None, "bash", serde_json::json!({})),
1217            tool_results(&["ok"]),
1218            user("hi"),
1219        ];
1220        let msgs = convert_messages(&input).unwrap();
1221        assert_eq!(msgs.len(), 2);
1222        assert!(matches!(msgs[1].role, AnthropicRole::User));
1223        assert_eq!(msgs[1].content.len(), 2);
1224        // tool_result blocks must come first in a user message.
1225        assert!(matches!(
1226            msgs[1].content[0],
1227            AnthropicContent::ToolResult { .. }
1228        ));
1229        assert!(matches!(msgs[1].content[1], AnthropicContent::Text { .. }));
1230    }
1231
1232    /// History carries no tool ids, but the wire must: minted ids have to be
1233    /// legal under Anthropic's `^[a-zA-Z0-9_-]+$` and keep the `tool_use` and
1234    /// its `tool_result` naming each other.
1235    #[test]
1236    fn wire_carries_legal_minted_ids_that_pair_up() {
1237        let input = [
1238            user("hello"),
1239            assistant_tool(None, "bash", serde_json::json!({})),
1240            tool_results(&["ok"]),
1241        ];
1242        let json = serde_json::to_value(convert_messages(&input).unwrap()).unwrap();
1243
1244        let tool_use_id = json[1]["content"][0]["id"].as_str().unwrap();
1245        assert!(
1246            is_legal_anthropic_id(tool_use_id),
1247            "tool_use id {tool_use_id:?} violates ^[a-zA-Z0-9_-]+$"
1248        );
1249        assert_eq!(tool_use_id, json[2]["content"][0]["tool_use_id"]);
1250    }
1251
1252    #[test]
1253    fn message_start_usage_is_captured() {
1254        let mut acc = StreamAccumulator::default();
1255        let event: AnthropicStreamEvent = serde_json::from_str(
1256            r#"{"type":"message_start","message":{"role":"assistant","usage":{"input_tokens":2095,"cache_read_input_tokens":100,"cache_creation_input_tokens":0,"output_tokens":1}}}"#,
1257        )
1258        .unwrap();
1259        let usage = acc
1260            .handle_event(event)
1261            .unwrap()
1262            .into_iter()
1263            .find_map(|p| match p {
1264                MessagePart::Usage(u) => Some(u),
1265                _ => None,
1266            })
1267            .expect("message_start should emit usage");
1268        assert_eq!(usage.input_tokens, 2195);
1269        assert_eq!(usage.cached_input_tokens, 100);
1270        assert_eq!(usage.context_tokens(), 2195);
1271    }
1272
1273    /// The stream opens thinking with `signature: ""` and sends the real value
1274    /// as a `signature_delta` after the text; dropping it stores an empty string.
1275    #[test]
1276    fn signature_delta_completes_the_thinking_signature() {
1277        let mut acc = StreamAccumulator::default();
1278        let mut message = MessageAccumulator::default();
1279        message.push(&MessagePart::MessageStart).unwrap();
1280        for event in [
1281            r#"{"type":"message_start","message":{"usage":{"input_tokens":1}}}"#,
1282            r#"{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}"#,
1283            r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":"step"}}"#,
1284            r#"{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"c2ln"}}"#,
1285            r#"{"type":"content_block_stop","index":0}"#,
1286            r#"{"type":"message_delta","delta":{"stop_reason":"max_tokens"},"usage":{"output_tokens":5}}"#,
1287            r#"{"type":"message_stop"}"#,
1288        ] {
1289            for part in acc.handle_data(event).unwrap() {
1290                message.push(&part).unwrap();
1291            }
1292        }
1293        let output = message.finish().unwrap();
1294        assert!(matches!(
1295            output.content.as_slice(),
1296            [Content::Thinking { text, signature: Some(signature), redacted: false }]
1297                if text == "step" && signature == "c2ln"
1298        ));
1299    }
1300
1301    #[test]
1302    fn thinking_delta_maps_to_content_delta() {
1303        let mut acc = StreamAccumulator::default();
1304        let parts = acc
1305            .handle_event(AnthropicStreamEvent::ContentBlockStart {
1306                index: 0,
1307                content_block: AnthropicStreamContentBlock::Thinking {
1308                    thinking: String::new(),
1309                    signature: Some("sig123".into()),
1310                },
1311            })
1312            .unwrap();
1313        assert!(matches!(
1314            &parts[0],
1315            MessagePart::ContentStart(ContentStart::Thinking {
1316                index: 0,
1317                signature: Some(s),
1318                redacted: false,
1319            }) if s == "sig123"
1320        ));
1321
1322        let parts = acc
1323            .handle_event(AnthropicStreamEvent::ContentBlockDelta {
1324                index: 0,
1325                delta: AnthropicDelta::ThinkingDelta {
1326                    thinking: "step 1".into(),
1327                },
1328            })
1329            .unwrap();
1330        expect_thinking_delta(&parts[0], 0, "step 1");
1331
1332        let parts = acc
1333            .handle_event(AnthropicStreamEvent::ContentBlockStart {
1334                index: 1,
1335                content_block: AnthropicStreamContentBlock::Text { text: "hi".into() },
1336            })
1337            .unwrap();
1338        // content_index is remapped: thinking occupied content slot 0, text is 1.
1339        assert!(matches!(
1340            &parts[0],
1341            MessagePart::ContentStart(ContentStart::Text { index: 1 })
1342        ));
1343        expect_text_delta(&parts[1], 1, "hi");
1344    }
1345
1346    #[test]
1347    fn test_stream_accumulator_text_and_tool_index_remap() {
1348        let mut acc = StreamAccumulator::default();
1349
1350        let items = acc
1351            .handle_event(AnthropicStreamEvent::ContentBlockStart {
1352                index: 0,
1353                content_block: AnthropicStreamContentBlock::Text {
1354                    text: String::new(),
1355                },
1356            })
1357            .unwrap();
1358        assert!(matches!(
1359            items[0],
1360            MessagePart::ContentStart(ContentStart::Text { index: 0 })
1361        ));
1362
1363        let items = acc
1364            .handle_event(AnthropicStreamEvent::ContentBlockDelta {
1365                index: 0,
1366                delta: AnthropicDelta::TextDelta { text: "Hi".into() },
1367            })
1368            .unwrap();
1369        expect_text_delta(&items[0], 0, "Hi");
1370
1371        let items = acc
1372            .handle_event(AnthropicStreamEvent::ContentBlockStart {
1373                index: 1,
1374                content_block: AnthropicStreamContentBlock::ToolUse {
1375                    name: "get_weather".into(),
1376                    input: serde_json::json!({}),
1377                },
1378            })
1379            .unwrap();
1380        // Remapped tool index: tool uses get their own index space.
1381        expect_tool_start(&items[0], 0, "get_weather");
1382
1383        let items = acc
1384            .handle_event(AnthropicStreamEvent::ContentBlockDelta {
1385                index: 1,
1386                delta: AnthropicDelta::InputJsonDelta {
1387                    partial_json: r#"{"city":"SF"}"#.into(),
1388                },
1389            })
1390            .unwrap();
1391        expect_tool_args_delta(&items[0], 0, r#"{"city":"SF"}"#);
1392
1393        acc.handle_event(AnthropicStreamEvent::MessageDelta {
1394            delta: AnthropicMessageDelta {
1395                stop_reason: Some(AnthropicStopReason::ToolUse),
1396            },
1397            usage: None,
1398        })
1399        .unwrap();
1400        acc.handle_event(AnthropicStreamEvent::MessageStop).unwrap();
1401        acc.finish().unwrap();
1402    }
1403}