1use std::sync::Arc;
10
11use super::driver_core::{Slot, SlotMap, SseAccumulator};
12use super::*;
13
14#[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 #[serde(default = "default_max_request_bytes")]
23 pub max_request_bytes: usize,
24 #[serde(default)]
26 pub retry: RetryPolicy,
27 pub effort: Option<Effort>,
36}
37
38impl Default for AnthropicBackendConfig {
39 fn default() -> Self {
40 Self {
41 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
53pub 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 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 fn message_request(&self, messages: &[AnthropicMessage]) -> reqwest::RequestBuilder {
102 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 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
162struct MessageRun {
171 role: AnthropicRole,
172 content: Vec<AnthropicContent>,
173}
174
175fn 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 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 if blocks.is_empty() {
228 continue;
229 }
230 (AnthropicRole::Assistant, blocks)
231 }
232 };
233
234 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 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#[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 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 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 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 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#[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#[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 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#[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#[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 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#[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 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
751fn 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#[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 #[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
806fn 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 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 ThinkingMode::Effort | ThinkingMode::None => (None, None),
836 }
837}
838
839#[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 #[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#[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 assert_eq!(json[2]["content"][0]["cache_control"]["type"], "ephemeral");
967 assert_eq!(json[1]["content"][0]["cache_control"]["type"], "ephemeral");
968 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 assert_eq!(json[1]["content"][1]["cache_control"]["type"], "ephemeral");
1011 assert!(json[1]["content"][0].get("cache_control").is_none());
1012 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 #[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 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 assert!(matches!(
1226 msgs[1].content[0],
1227 AnthropicContent::ToolResult { .. }
1228 ));
1229 assert!(matches!(msgs[1].content[1], AnthropicContent::Text { .. }));
1230 }
1231
1232 #[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 #[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 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 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}