1use crate::error::{Error, Result};
2use crate::message::{Content, ContentPart, Message};
3use crate::model::Sonnet35Version;
4use crate::provider::HTTPProvider;
5use crate::{Chat, Claude, LlmToolInfo};
6use reqwest::{Method, Request, Url};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::env;
10use tracing::{debug, error, info, instrument, trace, warn};
11
12#[derive(Debug, Clone)]
14pub struct AnthropicConfig {
15 pub api_key: String,
17 pub base_url: String,
19 pub api_version: String,
21}
22
23impl Default for AnthropicConfig {
24 fn default() -> Self {
25 Self {
26 api_key: env::var("ANTHROPIC_API_KEY").unwrap_or_default(),
27 base_url: "https://api.anthropic.com/v1".to_string(),
28 api_version: "2023-06-01".to_string(),
29 }
30 }
31}
32
33#[derive(Debug, Clone)]
35pub struct AnthropicProvider {
36 config: AnthropicConfig,
38}
39
40impl AnthropicProvider {
41 #[instrument(level = "debug")]
53 pub fn new() -> Self {
54 info!("Creating new AnthropicProvider with default configuration");
55 let config = AnthropicConfig::default();
56 debug!("API key set: {}", !config.api_key.is_empty());
57 debug!("Base URL: {}", config.base_url);
58 debug!("API version: {}", config.api_version);
59
60 Self { config }
61 }
62
63 #[instrument(skip(config), level = "debug")]
79 pub fn with_config(config: AnthropicConfig) -> Self {
80 info!("Creating new AnthropicProvider with custom configuration");
81 debug!("API key set: {}", !config.api_key.is_empty());
82 debug!("Base URL: {}", config.base_url);
83 debug!("API version: {}", config.api_version);
84
85 Self { config }
86 }
87}
88
89impl Default for AnthropicProvider {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl HTTPProvider<Claude> for AnthropicProvider {
96 fn accept(&self, model: Claude, chat: &Chat) -> Result<Request> {
97 info!("Creating request for Claude model: {:?}", model);
98 debug!("Messages in chat history: {}", chat.history.len());
99
100 let url_str = format!("{}/messages", self.config.base_url);
101 debug!("Parsing URL: {}", url_str);
102 let url = match Url::parse(&url_str) {
103 Ok(url) => {
104 debug!("URL parsed successfully: {}", url);
105 url
106 }
107 Err(e) => {
108 error!("Failed to parse URL '{}': {}", url_str, e);
109 return Err(e.into());
110 }
111 };
112
113 let mut request = Request::new(Method::POST, url);
114 debug!("Created request: {} {}", request.method(), request.url());
115
116 debug!("Setting request headers");
118 let api_key_header = match self.config.api_key.parse() {
119 Ok(header) => header,
120 Err(e) => {
121 error!("Invalid API key format: {}", e);
122 return Err(Error::Authentication("Invalid API key format".into()));
123 }
124 };
125
126 let content_type_header = match "application/json".parse() {
127 Ok(header) => header,
128 Err(e) => {
129 error!("Failed to set content type: {}", e);
130 return Err(Error::Other("Failed to set content type".into()));
131 }
132 };
133
134 let api_version_header = match self.config.api_version.parse() {
135 Ok(header) => header,
136 Err(e) => {
137 error!("Invalid API version format: {}", e);
138 return Err(Error::Other("Invalid API version format".into()));
139 }
140 };
141
142 request.headers_mut().insert("x-api-key", api_key_header);
143 request
144 .headers_mut()
145 .insert("Content-Type", content_type_header);
146 request
147 .headers_mut()
148 .insert("anthropic-version", api_version_header);
149
150 trace!("Request headers set: {:#?}", request.headers());
151
152 debug!("Creating request payload");
154 let payload = match self.create_request_payload(model, chat) {
155 Ok(payload) => {
156 debug!("Request payload created successfully");
157 trace!("Model: {}", payload.model);
158 trace!("Max tokens: {:?}", payload.max_tokens);
159 trace!("System prompt present: {}", payload.system.is_some());
160 trace!("Number of messages: {}", payload.messages.len());
161 payload
162 }
163 Err(e) => {
164 error!("Failed to create request payload: {}", e);
165 return Err(e);
166 }
167 };
168
169 debug!("Serializing request payload");
171 let body_bytes = match serde_json::to_vec(&payload) {
172 Ok(bytes) => {
173 debug!("Payload serialized successfully ({} bytes)", bytes.len());
174 bytes
175 }
176 Err(e) => {
177 error!("Failed to serialize payload: {}", e);
178 return Err(Error::Serialization(e));
179 }
180 };
181
182 *request.body_mut() = Some(body_bytes.into());
183 info!("Request created successfully");
184
185 Ok(request)
186 }
187
188 fn parse(&self, raw_response_text: String) -> Result<Message> {
189 info!("Parsing response from Anthropic API");
190 trace!("Raw response: {}", raw_response_text);
191
192 if raw_response_text.contains("\"error\"") {
194 let error_response: serde_json::Value = match serde_json::from_str(&raw_response_text) {
195 Ok(err) => err,
196 Err(e) => {
197 error!("Failed to parse error response: {}", e);
198 error!("Raw response: {}", raw_response_text);
199 return Err(Error::Serialization(e));
200 }
201 };
202
203 if let Some(error) = error_response.get("error") {
204 if let Some(message) = error.get("message") {
205 let error_message = message.as_str().unwrap_or("Unknown error");
206 error!("Anthropic API returned an error: {}", error_message);
207 return Err(Error::ProviderUnavailable(error_message.to_string()));
208 }
209 }
210
211 error!("Unknown error format in response: {}", raw_response_text);
212 return Err(Error::ProviderUnavailable(
213 "Unknown error from Anthropic API".to_string(),
214 ));
215 }
216
217 debug!("Deserializing response JSON");
218 let anthropic_response = match serde_json::from_str::<AnthropicResponse>(&raw_response_text)
219 {
220 Ok(response) => {
221 debug!("Response deserialized successfully");
222 debug!("Response ID: {}", response.id);
223 debug!("Response model: {}", response.model);
224 debug!("Stop reason: {:?}", response.stop_reason);
225 debug!("Content parts: {}", response.content.len());
226 debug!("Input tokens: {}", response.usage.input_tokens);
227 debug!("Output tokens: {}", response.usage.output_tokens);
228 response
229 }
230 Err(e) => {
231 error!("Failed to deserialize response: {}", e);
232 error!("Raw response: {}", raw_response_text);
233 return Err(Error::Serialization(e));
234 }
235 };
236
237 debug!("Converting Anthropic response to Message");
239 let message = Message::from(&anthropic_response);
240
241 info!("Response parsed successfully");
242 trace!("Response message processed");
243
244 Ok(message)
245 }
246}
247
248impl AnthropicProvider {
249 #[instrument(level = "debug")]
250 fn id_for_model(model: Claude) -> &'static str {
251 let model_id = match model {
252 Claude::Sonnet37 { .. } => "claude-3-7-sonnet-latest",
253 Claude::Sonnet35 {
254 version: Sonnet35Version::V1,
255 } => "claude-3-5-sonnet-20240620",
256 Claude::Sonnet35 {
257 version: Sonnet35Version::V2,
258 } => "claude-3-5-sonnet-20241022",
259 Claude::Opus3 => "claude-3-opus-latest",
260 Claude::Haiku3 => "claude-3-haiku-20240307",
261 Claude::Haiku35 => "claude-3-5-haiku-latest",
262 };
263
264 debug!("Mapped Claude model to Anthropic model ID: {}", model_id);
265 model_id
266 }
267
268 #[instrument(skip(self, chat), level = "debug")]
273 fn create_request_payload(&self, model: Claude, chat: &Chat) -> Result<AnthropicRequest> {
274 info!("Creating request payload for chat with Claude model");
275 debug!("System prompt length: {}", chat.system_prompt.len());
276 debug!("Messages in history: {}", chat.history.len());
277 debug!("Max output tokens: {}", chat.max_output_tokens);
278
279 let system = if chat.system_prompt.is_empty() {
281 debug!("No system prompt provided");
282 None
283 } else {
284 debug!("Including system prompt in request");
285 trace!("System prompt: {}", chat.system_prompt);
286 Some(chat.system_prompt.clone())
287 };
288
289 debug!("Converting messages to Anthropic format");
291 let messages: Vec<AnthropicMessage> = chat
292 .history
293 .iter()
294 .filter(|msg| !matches!(msg, Message::System { .. })) .map(|msg| {
296 trace!("Converting message with role: {}", msg.role_str());
297 AnthropicMessage::from(msg)
298 })
299 .collect();
300
301 debug!("Converted {} messages for the request", messages.len());
302
303 let model_id = Self::id_for_model(model).to_string();
305 debug!("Using model ID: {}", model_id);
306
307 let tools = chat
309 .tools
310 .as_ref()
311 .map(|tools| tools.iter().map(AnthropicTool::from).collect());
312 debug!("Tools configured: {:?}", tools);
313
314 debug!("Processing tool_choice: {:?}", chat.tool_choice);
319 let tool_choice = if let Some(choice) = &chat.tool_choice {
320 let anthropic_choice = match choice {
321 crate::tool::ToolChoice::Auto => {
322 debug!("Setting tool_choice to type:auto");
323 Some(serde_json::json!({ "type": "auto" }))
324 }
325 crate::tool::ToolChoice::Any => {
327 debug!("Setting tool_choice to type:any");
328 Some(serde_json::json!({ "type": "any" }))
329 }
330 crate::tool::ToolChoice::None => {
331 debug!("Setting tool_choice to type:none");
332 Some(serde_json::json!({ "type": "none" }))
333 }
334 crate::tool::ToolChoice::Specific(name) => {
335 debug!("Setting tool_choice to specific tool: {}", name);
337 Some(serde_json::json!({
338 "type": "function",
339 "function": { "name": name }
340 }))
341 }
342 };
343 debug!("Anthropic tool_choice value: {:?}", anthropic_choice);
344 anthropic_choice
345 } else if tools.is_some() {
346 debug!("No tool_choice specified but tools are present, defaulting to type:auto");
348 Some(serde_json::json!({ "type": "auto" }))
349 } else {
350 debug!("No tool_choice and no tools, setting to None");
351 None
352 };
353
354 debug!("Final tool_choice value: {:?}", tool_choice);
355
356 debug!("Creating AnthropicRequest");
358 let request = AnthropicRequest {
359 model: model_id,
360 messages,
361 system,
362 max_tokens: Some(chat.max_output_tokens),
363 temperature: None,
364 top_p: None,
365 top_k: None,
366 tools,
367 tool_choice,
368 };
369
370 info!("Request payload created successfully");
371 Ok(request)
372 }
373}
374
375#[derive(Debug, Clone, Serialize, Deserialize)]
377pub(crate) struct AnthropicMessage {
378 pub role: String,
380 pub content: Vec<AnthropicContentPart>,
382}
383
384#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(tag = "type")]
387pub(crate) enum AnthropicContentPart {
388 #[serde(rename = "text")]
390 Text {
391 text: String,
393 },
394 #[serde(rename = "image")]
396 Image {
397 source: AnthropicImageSource,
399 },
400 #[serde(rename = "tool_result")]
402 ToolResult(AnthropicToolResponse),
403 #[serde(rename = "tool_use")]
405 ToolUse {
406 id: String,
408 name: String,
410 input: serde_json::Value,
412 },
413}
414
415impl AnthropicContentPart {
416 fn text(text: String) -> Self {
418 AnthropicContentPart::Text { text }
419 }
420
421 fn image(url: String) -> Self {
423 AnthropicContentPart::Image {
424 source: AnthropicImageSource {
425 type_field: "base64".to_string(),
426 media_type: "image/jpeg".to_string(),
427 data: url,
428 },
429 }
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize)]
435pub(crate) struct AnthropicImageSource {
436 #[serde(rename = "type")]
438 pub type_field: String,
439 pub media_type: String,
441 pub data: String,
443}
444
445#[derive(Debug, Clone, Serialize, Deserialize)]
447pub(crate) struct AnthropicToolResponse {
448 #[serde(rename = "type")]
450 pub type_field: String,
451 #[serde(rename = "tool_use_id")]
453 pub tool_call_id: String,
454 pub content: String,
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize)]
460pub(crate) struct AnthropicTool {
461 pub name: String,
463 pub description: String,
465 pub input_schema: serde_json::Value,
467}
468
469#[derive(Debug, Serialize, Deserialize)]
471pub(crate) struct AnthropicRequest {
472 pub model: String,
474 pub messages: Vec<AnthropicMessage>,
476 #[serde(skip_serializing_if = "Option::is_none")]
478 pub system: Option<String>,
479 #[serde(skip_serializing_if = "Option::is_none")]
481 pub max_tokens: Option<usize>,
482 #[serde(skip_serializing_if = "Option::is_none")]
484 pub temperature: Option<f32>,
485 #[serde(skip_serializing_if = "Option::is_none")]
487 pub top_p: Option<f32>,
488 #[serde(skip_serializing_if = "Option::is_none")]
490 pub top_k: Option<u32>,
491 #[serde(skip_serializing_if = "Option::is_none")]
493 pub tools: Option<Vec<AnthropicTool>>,
494 #[serde(skip_serializing_if = "Option::is_none")]
496 pub tool_choice: Option<serde_json::Value>,
497}
498
499#[derive(Debug, Serialize, Deserialize)]
501pub(crate) struct AnthropicResponse {
502 pub id: String,
504 #[serde(rename = "type")]
506 pub type_field: String,
507 pub role: String,
509 pub model: String,
511 pub stop_reason: Option<String>,
513 pub content: Vec<AnthropicResponseContent>,
515 pub usage: AnthropicUsage,
517}
518
519#[derive(Debug, Serialize, Deserialize)]
521#[serde(tag = "type")]
522pub(crate) enum AnthropicResponseContent {
523 #[serde(rename = "text")]
525 Text {
526 text: String,
528 },
529 #[serde(rename = "tool_use")]
531 ToolUse {
532 id: String,
534 name: String,
536 input: serde_json::Value,
538 },
539}
540
541#[derive(Debug, Serialize, Deserialize)]
543pub(crate) struct AnthropicUsage {
544 pub input_tokens: u32,
546 pub output_tokens: u32,
548}
549
550impl From<&Message> for AnthropicMessage {
552 fn from(msg: &Message) -> Self {
553 let role = match msg {
554 Message::System { .. } => "system",
555 Message::User { .. } | Message::Tool { .. } => "user", Message::Assistant { .. } => "assistant",
557 }
558 .to_string();
559
560 let content = match msg {
561 Message::System { content, .. } => vec![AnthropicContentPart::text(content.clone())],
562 Message::User { content, .. } => match content {
563 Content::Text(text) => vec![AnthropicContentPart::text(text.clone())],
564 Content::Parts(parts) => parts
565 .iter()
566 .map(|part| match part {
567 ContentPart::Text { text } => AnthropicContentPart::text(text.clone()),
568 ContentPart::ImageUrl { image_url } => {
569 AnthropicContentPart::image(image_url.url.clone())
570 }
571 })
572 .collect(),
573 },
574 Message::Assistant {
575 content,
576 tool_calls,
577 ..
578 } => {
579 let mut parts: Vec<AnthropicContentPart> = match content {
581 Some(Content::Text(text)) => vec![AnthropicContentPart::text(text.clone())],
582 Some(Content::Parts(parts)) => parts
583 .iter()
584 .map(|part| match part {
585 ContentPart::Text { text } => AnthropicContentPart::text(text.clone()),
586 ContentPart::ImageUrl { image_url } => {
587 AnthropicContentPart::image(image_url.url.clone())
588 }
589 })
590 .collect(),
591 None => Vec::new(),
592 };
593
594 for call in tool_calls {
596 let parsed_args: serde_json::Value =
597 serde_json::from_str(&call.function.arguments).unwrap_or_else(|_| {
598 serde_json::Value::String(call.function.arguments.clone())
600 });
601
602 parts.push(AnthropicContentPart::ToolUse {
603 id: call.id.clone(),
604 name: call.function.name.clone(),
605 input: parsed_args,
606 });
607 }
608
609 if parts.is_empty() {
612 parts.push(AnthropicContentPart::text("(no content)".to_string()));
613 }
614
615 parts
616 },
617 Message::Tool {
618 tool_call_id,
619 content,
620 ..
621 } => {
622 vec![AnthropicContentPart::ToolResult(AnthropicToolResponse {
624 type_field: "tool_result".to_string(),
625 tool_call_id: tool_call_id.clone(),
626 content: content.clone(),
627 })]
628 }
629 };
630
631 AnthropicMessage { role, content }
632 }
633}
634
635impl From<&AnthropicResponseContent> for ContentPart {
637 fn from(content: &AnthropicResponseContent) -> Self {
638 match content {
639 AnthropicResponseContent::Text { text } => ContentPart::text(text.clone()),
640 AnthropicResponseContent::ToolUse { id, name, input } => {
641 let text = format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"input\":{input}}}");
643 ContentPart::text(text)
644 }
645 }
646 }
647}
648
649impl From<&AnthropicResponse> for Message {
651 fn from(response: &AnthropicResponse) -> Self {
652 let mut text_content = Vec::new();
654 let mut tool_calls = Vec::new();
655
656 for content_part in &response.content {
657 match content_part {
658 AnthropicResponseContent::Text { text } => {
659 text_content.push(ContentPart::text(text.clone()));
660 }
661 AnthropicResponseContent::ToolUse { id, name, input } => {
662 let function = crate::message::Function {
664 name: name.clone(),
665 arguments: serde_json::to_string(input).unwrap_or_default(),
666 };
667
668 let tool_call = crate::message::ToolCall {
669 id: id.clone(),
670 tool_type: "function".to_string(),
671 function,
672 };
673
674 tool_calls.push(tool_call);
675 }
676 }
677 }
678
679 let content = if text_content.is_empty() {
681 None
682 } else if text_content.len() == 1 {
683 match &text_content[0] {
684 ContentPart::Text { text } => Some(Content::Text(text.clone())),
685 ContentPart::ImageUrl { .. } => Some(Content::Parts(text_content)),
686 }
687 } else {
688 Some(Content::Parts(text_content))
689 };
690
691 let mut msg = match response.role.as_str() {
693 "assistant" => {
694 if tool_calls.is_empty() {
695 let content_to_use = content.unwrap_or(Content::Text(String::new()));
696 match content_to_use {
697 Content::Text(text) => Message::assistant(text),
698 Content::Parts(_) => Message::Assistant {
699 content: Some(content_to_use),
700 tool_calls: Vec::new(),
701 metadata: HashMap::default(),
702 },
703 }
704 } else {
705 Message::Assistant {
706 content,
707 tool_calls,
708 metadata: HashMap::default(),
709 }
710 }
711 }
712 _ => match content {
714 Some(Content::Text(text)) => Message::user(text),
715 Some(Content::Parts(parts)) => Message::User {
716 content: Content::Parts(parts),
717 name: None,
718 metadata: HashMap::default(),
719 },
720 None => Message::user(""),
721 },
722 };
723
724 msg = msg.with_metadata(
726 "input_tokens",
727 serde_json::Value::Number(response.usage.input_tokens.into()),
728 );
729 msg = msg.with_metadata(
730 "output_tokens",
731 serde_json::Value::Number(response.usage.output_tokens.into()),
732 );
733
734 msg
735 }
736}
737
738impl From<&LlmToolInfo> for AnthropicTool {
739 fn from(value: &LlmToolInfo) -> Self {
740 AnthropicTool {
741 name: value.name.clone(),
742 description: value.description.clone(),
743 input_schema: value.parameters.clone(),
744 }
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use super::*;
751
752 use crate::message::{Content, ContentPart, Message};
753
754 #[test]
755 fn test_message_to_anthropic_conversion() {
756 let msg = Message::user("Hello, world!");
758 let anthropic_msg = AnthropicMessage::from(&msg);
759
760 assert_eq!(anthropic_msg.role, "user");
761 assert_eq!(anthropic_msg.content.len(), 1);
762 match &anthropic_msg.content[0] {
763 AnthropicContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
764 _ => panic!("Expected text content"),
765 }
766
767 let parts = vec![ContentPart::text("Hello"), ContentPart::text("world")];
769 let msg = Message::user_with_parts(parts);
770 let anthropic_msg = AnthropicMessage::from(&msg);
771
772 assert_eq!(anthropic_msg.role, "user");
773 assert_eq!(anthropic_msg.content.len(), 2);
774
775 let msg = Message::system("You are a helpful assistant.");
777 let anthropic_msg = AnthropicMessage::from(&msg);
778
779 assert_eq!(anthropic_msg.role, "system");
780 assert_eq!(anthropic_msg.content.len(), 1);
781
782 let parts = vec![
784 ContentPart::text("Look at this image:"),
785 ContentPart::image_url("https://example.com/image.jpg"),
786 ];
787 let msg = Message::user_with_parts(parts);
788 let anthropic_msg = AnthropicMessage::from(&msg);
789
790 assert_eq!(anthropic_msg.role, "user");
791 assert_eq!(anthropic_msg.content.len(), 2);
792
793 match &anthropic_msg.content[1] {
795 AnthropicContentPart::Image { source } => {
796 assert_eq!(source.data, "https://example.com/image.jpg");
797 assert_eq!(source.type_field, "base64");
798 assert_eq!(source.media_type, "image/jpeg");
799 }
800 _ => panic!("Expected image content"),
801 }
802
803 let msg = Message::tool("tool_call_123", "The weather is sunny.");
805 let anthropic_msg = AnthropicMessage::from(&msg);
806
807 assert_eq!(anthropic_msg.role, "user");
808 assert_eq!(anthropic_msg.content.len(), 1);
809
810 match &anthropic_msg.content[0] {
812 AnthropicContentPart::ToolResult(tool_result) => {
813 assert_eq!(tool_result.type_field, "tool_result");
814 assert_eq!(tool_result.tool_call_id, "tool_call_123");
815 assert_eq!(tool_result.content, "The weather is sunny.");
816 }
817 _ => panic!("Expected tool result content"),
818 }
819 }
820
821 #[test]
822 fn test_anthropic_response_to_message() {
823 let response = AnthropicResponse {
825 id: "msg_123".to_string(),
826 type_field: "message".to_string(),
827 role: "assistant".to_string(),
828 model: "claude-3-opus-20240229".to_string(),
829 stop_reason: Some("end_turn".to_string()),
830 content: vec![AnthropicResponseContent::Text {
831 text: "I'm Claude, an AI assistant.".to_string(),
832 }],
833 usage: AnthropicUsage {
834 input_tokens: 10,
835 output_tokens: 20,
836 },
837 };
838
839 let msg = Message::from(&response);
840
841 match &msg {
843 Message::Assistant {
844 content,
845 tool_calls,
846 ..
847 } => {
848 match content {
850 Some(Content::Text(text)) => assert_eq!(text, "I'm Claude, an AI assistant."),
851 _ => panic!("Expected text content"),
852 }
853 assert!(tool_calls.is_empty());
855 }
856 _ => panic!("Expected Assistant variant"),
857 }
858
859 match &msg {
861 Message::Assistant { metadata, .. } => {
862 assert_eq!(metadata["input_tokens"], 10);
863 assert_eq!(metadata["output_tokens"], 20);
864 }
865 _ => panic!("Expected Assistant variant"),
866 }
867
868 let response = AnthropicResponse {
870 id: "msg_123".to_string(),
871 type_field: "message".to_string(),
872 role: "assistant".to_string(),
873 model: "claude-3-opus-20240229".to_string(),
874 stop_reason: Some("end_turn".to_string()),
875 content: vec![
876 AnthropicResponseContent::Text {
877 text: "Here's the information:".to_string(),
878 },
879 AnthropicResponseContent::ToolUse {
880 id: "tool_123".to_string(),
881 name: "get_weather".to_string(),
882 input: serde_json::json!({
883 "location": "San Francisco",
884 }),
885 },
886 ],
887 usage: AnthropicUsage {
888 input_tokens: 15,
889 output_tokens: 30,
890 },
891 };
892
893 let msg = Message::from(&response);
894
895 match &msg {
897 Message::Assistant {
898 content,
899 tool_calls,
900 ..
901 } => {
902 match content {
904 Some(Content::Text(text)) => {
905 assert_eq!(text, "Here's the information:");
906 }
907 Some(Content::Parts(parts)) => {
908 assert_eq!(parts.len(), 1);
909 match &parts[0] {
910 ContentPart::Text { text } => {
911 assert_eq!(text, "Here's the information:")
912 }
913 _ => panic!("Expected text content"),
914 }
915 }
916 _ => panic!("Expected content"),
917 }
918
919 assert_eq!(tool_calls.len(), 1);
921
922 let tool_call = &tool_calls[0];
923 assert_eq!(tool_call.id, "tool_123");
924 assert_eq!(tool_call.tool_type, "function");
925 assert_eq!(tool_call.function.name, "get_weather");
926
927 let arguments: serde_json::Value =
929 serde_json::from_str(&tool_call.function.arguments).unwrap();
930 assert_eq!(arguments["location"], "San Francisco");
931 }
932 _ => panic!("Expected Assistant variant"),
933 }
934 }
935
936 #[test]
937 fn test_anthropic_response_with_tool_use_only() {
938 let response = AnthropicResponse {
940 id: "msg_123".to_string(),
941 type_field: "message".to_string(),
942 role: "assistant".to_string(),
943 model: "claude-3-opus-20240229".to_string(),
944 stop_reason: Some("end_turn".to_string()),
945 content: vec![AnthropicResponseContent::ToolUse {
946 id: "tool_xyz".to_string(),
947 name: "calculate".to_string(),
948 input: serde_json::json!({
949 "expression": "2+2",
950 }),
951 }],
952 usage: AnthropicUsage {
953 input_tokens: 5,
954 output_tokens: 10,
955 },
956 };
957
958 let msg = Message::from(&response);
959
960 match &msg {
962 Message::Assistant {
963 content,
964 tool_calls,
965 ..
966 } => {
967 assert!(
969 content.is_none()
970 || (match content {
971 Some(Content::Parts(parts)) => parts.is_empty(),
972 _ => false,
973 })
974 );
975
976 assert_eq!(tool_calls.len(), 1);
978
979 let tool_call = &tool_calls[0];
980 assert_eq!(tool_call.id, "tool_xyz");
981 assert_eq!(tool_call.function.name, "calculate");
982
983 let arguments: serde_json::Value =
985 serde_json::from_str(&tool_call.function.arguments).unwrap();
986 assert_eq!(arguments["expression"], "2+2");
987 }
988 _ => panic!("Expected Assistant variant"),
989 }
990 }
991
992 #[test]
993 fn test_manual_message_conversion() {
994 let text_msg = Message::user("Hello, world!");
998 let anthropic_msg = AnthropicMessage::from(&text_msg);
999
1000 assert_eq!(anthropic_msg.role, "user");
1002 assert_eq!(anthropic_msg.content.len(), 1);
1003 match &anthropic_msg.content[0] {
1004 AnthropicContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
1005 _ => panic!("Expected text content"),
1006 }
1007
1008 let response = AnthropicResponse {
1010 id: "msg_123".to_string(),
1011 type_field: "message".to_string(),
1012 role: "assistant".to_string(),
1013 model: "claude-3-opus-20240229".to_string(),
1014 stop_reason: Some("end_turn".to_string()),
1015 content: vec![AnthropicResponseContent::Text {
1016 text: "I'm here to help!".to_string(),
1017 }],
1018 usage: AnthropicUsage {
1019 input_tokens: 5,
1020 output_tokens: 15,
1021 },
1022 };
1023
1024 let lib_msg = Message::from(&response);
1026
1027 match &lib_msg {
1029 Message::Assistant {
1030 content,
1031 tool_calls,
1032 metadata,
1033 } => {
1034 match content {
1036 Some(Content::Text(text)) => assert_eq!(text, "I'm here to help!"),
1037 _ => panic!("Expected text content"),
1038 }
1039
1040 assert!(tool_calls.is_empty());
1042
1043 assert_eq!(metadata["input_tokens"], 5);
1045 assert_eq!(metadata["output_tokens"], 15);
1046 }
1047 _ => panic!("Expected Assistant variant"),
1048 }
1049 }
1050
1051 #[test]
1052 fn test_assistant_message_with_tool_calls_converts_to_tool_use_blocks() {
1053 use crate::message::{Function, ToolCall};
1055
1056 let tool_call = ToolCall {
1057 id: "toolu_123".to_string(),
1058 tool_type: "function".to_string(),
1059 function: Function {
1060 name: "observe".to_string(),
1061 arguments: "{\"delay\":{\"delay_seconds\":0.0}}".to_string(),
1062 },
1063 };
1064
1065 let assistant_msg = Message::assistant_with_tool_calls(vec![tool_call.clone()]);
1066
1067 let anthropic_msg = AnthropicMessage::from(&assistant_msg);
1069
1070 assert_eq!(anthropic_msg.role, "assistant");
1072
1073 assert_eq!(anthropic_msg.content.len(), 1);
1075
1076 match &anthropic_msg.content[0] {
1077 AnthropicContentPart::ToolUse { id, name, input } => {
1078 assert_eq!(id, &tool_call.id);
1079 assert_eq!(name, &tool_call.function.name);
1080
1081 let expected: serde_json::Value =
1083 serde_json::from_str(&tool_call.function.arguments).unwrap();
1084 assert_eq!(input, &expected);
1085 }
1086 _ => panic!("Expected first content block to be a tool_use"),
1087 }
1088 }
1089
1090 #[test]
1091 fn test_headers() {
1092 let config = AnthropicConfig {
1097 api_key: "test-api-key".to_string(),
1098 base_url: "https://api.anthropic.com/v1".to_string(),
1099 api_version: "2023-06-01".to_string(),
1100 };
1101 let provider = AnthropicProvider::with_config(config);
1102
1103 let model = Claude::Sonnet37 {
1105 use_extended_thinking: false,
1106 };
1107 let chat = Chat::default()
1108 .with_system_prompt("You are a helpful assistant.")
1109 .with_max_output_tokens(1024)
1110 .add_message(Message::user("Hello, how are you?"))
1111 .add_message(Message::assistant("I'm doing well, thank you for asking!"))
1112 .add_message(Message::user("Can you help me with a question?"));
1113
1114 let request = provider.accept(model, &chat).unwrap();
1116
1117 assert_eq!(request.method(), "POST");
1119 assert_eq!(
1120 request.url().as_str(),
1121 "https://api.anthropic.com/v1/messages"
1122 );
1123 assert_eq!(request.headers()["x-api-key"], "test-api-key");
1124 assert_eq!(request.headers()["anthropic-version"], "2023-06-01");
1125 assert_eq!(request.headers()["Content-Type"], "application/json");
1126 }
1127}