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, chat: Chat<Claude>) -> Result<Request> {
97 info!("Creating request for Claude model: {:?}", chat.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(&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, chat: &Chat<Claude>) -> 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(chat.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
313 debug!("Creating AnthropicRequest");
315 let request = AnthropicRequest {
316 model: model_id,
317 messages,
318 system,
319 max_tokens: Some(chat.max_output_tokens),
320 temperature: None,
321 top_p: None,
322 top_k: None,
323 tools,
324 };
325
326 info!("Request payload created successfully");
327 Ok(request)
328 }
329}
330
331#[derive(Debug, Clone, Serialize, Deserialize)]
333pub(crate) struct AnthropicMessage {
334 pub role: String,
336 pub content: Vec<AnthropicContentPart>,
338}
339
340#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(tag = "type")]
343pub(crate) enum AnthropicContentPart {
344 #[serde(rename = "text")]
346 Text {
347 text: String,
349 },
350 #[serde(rename = "image")]
352 Image {
353 source: AnthropicImageSource,
355 },
356 #[serde(rename = "tool_result")]
358 ToolResult(AnthropicToolResponse),
359}
360
361impl AnthropicContentPart {
362 fn text(text: String) -> Self {
364 AnthropicContentPart::Text { text }
365 }
366
367 fn image(url: String) -> Self {
369 AnthropicContentPart::Image {
370 source: AnthropicImageSource {
371 type_field: "base64".to_string(),
372 media_type: "image/jpeg".to_string(),
373 data: url,
374 },
375 }
376 }
377}
378
379#[derive(Debug, Clone, Serialize, Deserialize)]
381pub(crate) struct AnthropicImageSource {
382 #[serde(rename = "type")]
384 pub type_field: String,
385 pub media_type: String,
387 pub data: String,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub(crate) struct AnthropicToolResponse {
394 #[serde(rename = "type")]
396 pub type_field: String,
397 #[serde(rename = "tool_use_id")]
399 pub tool_call_id: String,
400 pub content: String,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize)]
406pub(crate) struct AnthropicTool {
407 pub name: String,
409 pub description: String,
411 pub input_schema: serde_json::Value,
413}
414
415#[derive(Debug, Serialize, Deserialize)]
417pub(crate) struct AnthropicRequest {
418 pub model: String,
420 pub messages: Vec<AnthropicMessage>,
422 #[serde(skip_serializing_if = "Option::is_none")]
424 pub system: Option<String>,
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub max_tokens: Option<usize>,
428 #[serde(skip_serializing_if = "Option::is_none")]
430 pub temperature: Option<f32>,
431 #[serde(skip_serializing_if = "Option::is_none")]
433 pub top_p: Option<f32>,
434 #[serde(skip_serializing_if = "Option::is_none")]
436 pub top_k: Option<u32>,
437 #[serde(skip_serializing_if = "Option::is_none")]
439 pub tools: Option<Vec<AnthropicTool>>,
440}
441
442#[derive(Debug, Serialize, Deserialize)]
444pub(crate) struct AnthropicResponse {
445 pub id: String,
447 #[serde(rename = "type")]
449 pub type_field: String,
450 pub role: String,
452 pub model: String,
454 pub stop_reason: Option<String>,
456 pub content: Vec<AnthropicResponseContent>,
458 pub usage: AnthropicUsage,
460}
461
462#[derive(Debug, Serialize, Deserialize)]
464#[serde(tag = "type")]
465pub(crate) enum AnthropicResponseContent {
466 #[serde(rename = "text")]
468 Text {
469 text: String,
471 },
472 #[serde(rename = "tool_use")]
474 ToolUse {
475 id: String,
477 name: String,
479 input: serde_json::Value,
481 },
482}
483
484#[derive(Debug, Serialize, Deserialize)]
486pub(crate) struct AnthropicUsage {
487 pub input_tokens: u32,
489 pub output_tokens: u32,
491}
492
493impl From<&Message> for AnthropicMessage {
495 fn from(msg: &Message) -> Self {
496 let role = match msg {
497 Message::System { .. } => "system",
498 Message::User { .. } | Message::Tool { .. } => "user", Message::Assistant { .. } => "assistant",
500 }
501 .to_string();
502
503 let content = match msg {
504 Message::System { content, .. } => vec![AnthropicContentPart::text(content.clone())],
505 Message::User { content, .. } => match content {
506 Content::Text(text) => vec![AnthropicContentPart::text(text.clone())],
507 Content::Parts(parts) => parts
508 .iter()
509 .map(|part| match part {
510 ContentPart::Text { text } => AnthropicContentPart::text(text.clone()),
511 ContentPart::ImageUrl { image_url } => {
512 AnthropicContentPart::image(image_url.url.clone())
513 }
514 })
515 .collect(),
516 },
517 Message::Assistant { content, .. } => match content {
518 Some(Content::Text(text)) => vec![AnthropicContentPart::text(text.clone())],
519 Some(Content::Parts(parts)) => parts
520 .iter()
521 .map(|part| match part {
522 ContentPart::Text { text } => AnthropicContentPart::text(text.clone()),
523 ContentPart::ImageUrl { image_url } => {
524 AnthropicContentPart::image(image_url.url.clone())
525 }
526 })
527 .collect(),
528 None => vec![AnthropicContentPart::text(String::new())],
529 },
530 Message::Tool {
531 tool_call_id,
532 content,
533 ..
534 } => {
535 vec![AnthropicContentPart::ToolResult(AnthropicToolResponse {
537 type_field: "tool_result".to_string(),
538 tool_call_id: tool_call_id.clone(),
539 content: content.clone(),
540 })]
541 }
542 };
543
544 AnthropicMessage { role, content }
545 }
546}
547
548impl From<&AnthropicResponseContent> for ContentPart {
550 fn from(content: &AnthropicResponseContent) -> Self {
551 match content {
552 AnthropicResponseContent::Text { text } => ContentPart::text(text.clone()),
553 AnthropicResponseContent::ToolUse { id, name, input } => {
554 let text = format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"input\":{input}}}");
556 ContentPart::text(text)
557 }
558 }
559 }
560}
561
562impl From<&AnthropicResponse> for Message {
564 fn from(response: &AnthropicResponse) -> Self {
565 let mut text_content = Vec::new();
567 let mut tool_calls = Vec::new();
568
569 for content_part in &response.content {
570 match content_part {
571 AnthropicResponseContent::Text { text } => {
572 text_content.push(ContentPart::text(text.clone()));
573 }
574 AnthropicResponseContent::ToolUse { id, name, input } => {
575 let function = crate::message::Function {
577 name: name.clone(),
578 arguments: serde_json::to_string(input).unwrap_or_default(),
579 };
580
581 let tool_call = crate::message::ToolCall {
582 id: id.clone(),
583 tool_type: "function".to_string(),
584 function,
585 };
586
587 tool_calls.push(tool_call);
588 }
589 }
590 }
591
592 let content = if text_content.is_empty() {
594 None
595 } else if text_content.len() == 1 {
596 match &text_content[0] {
597 ContentPart::Text { text } => Some(Content::Text(text.clone())),
598 ContentPart::ImageUrl { .. } => Some(Content::Parts(text_content)),
599 }
600 } else {
601 Some(Content::Parts(text_content))
602 };
603
604 let mut msg = match response.role.as_str() {
606 "assistant" => {
607 if tool_calls.is_empty() {
608 let content_to_use = content.unwrap_or(Content::Text(String::new()));
609 match content_to_use {
610 Content::Text(text) => Message::assistant(text),
611 Content::Parts(_) => Message::Assistant {
612 content: Some(content_to_use),
613 tool_calls: Vec::new(),
614 metadata: HashMap::default(),
615 },
616 }
617 } else {
618 Message::Assistant {
619 content,
620 tool_calls,
621 metadata: HashMap::default(),
622 }
623 }
624 }
625 _ => match content {
627 Some(Content::Text(text)) => Message::user(text),
628 Some(Content::Parts(parts)) => Message::User {
629 content: Content::Parts(parts),
630 name: None,
631 metadata: HashMap::default(),
632 },
633 None => Message::user(""),
634 },
635 };
636
637 msg = msg.with_metadata(
639 "input_tokens",
640 serde_json::Value::Number(response.usage.input_tokens.into()),
641 );
642 msg = msg.with_metadata(
643 "output_tokens",
644 serde_json::Value::Number(response.usage.output_tokens.into()),
645 );
646
647 msg
648 }
649}
650
651impl From<&LlmToolInfo> for AnthropicTool {
652 fn from(value: &LlmToolInfo) -> Self {
653 AnthropicTool {
654 name: value.name.clone(),
655 description: value.description.clone(),
656 input_schema: value.parameters.clone(),
657 }
658 }
659}
660
661#[cfg(test)]
662mod tests {
663 use super::*;
664
665 use crate::message::{Content, ContentPart, Message};
666
667 #[test]
668 fn test_message_to_anthropic_conversion() {
669 let msg = Message::user("Hello, world!");
671 let anthropic_msg = AnthropicMessage::from(&msg);
672
673 assert_eq!(anthropic_msg.role, "user");
674 assert_eq!(anthropic_msg.content.len(), 1);
675 match &anthropic_msg.content[0] {
676 AnthropicContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
677 _ => panic!("Expected text content"),
678 }
679
680 let parts = vec![ContentPart::text("Hello"), ContentPart::text("world")];
682 let msg = Message::user_with_parts(parts);
683 let anthropic_msg = AnthropicMessage::from(&msg);
684
685 assert_eq!(anthropic_msg.role, "user");
686 assert_eq!(anthropic_msg.content.len(), 2);
687
688 let msg = Message::system("You are a helpful assistant.");
690 let anthropic_msg = AnthropicMessage::from(&msg);
691
692 assert_eq!(anthropic_msg.role, "system");
693 assert_eq!(anthropic_msg.content.len(), 1);
694
695 let parts = vec![
697 ContentPart::text("Look at this image:"),
698 ContentPart::image_url("https://example.com/image.jpg"),
699 ];
700 let msg = Message::user_with_parts(parts);
701 let anthropic_msg = AnthropicMessage::from(&msg);
702
703 assert_eq!(anthropic_msg.role, "user");
704 assert_eq!(anthropic_msg.content.len(), 2);
705
706 match &anthropic_msg.content[1] {
708 AnthropicContentPart::Image { source } => {
709 assert_eq!(source.data, "https://example.com/image.jpg");
710 assert_eq!(source.type_field, "base64");
711 assert_eq!(source.media_type, "image/jpeg");
712 }
713 _ => panic!("Expected image content"),
714 }
715
716 let msg = Message::tool("tool_call_123", "The weather is sunny.");
718 let anthropic_msg = AnthropicMessage::from(&msg);
719
720 assert_eq!(anthropic_msg.role, "user");
721 assert_eq!(anthropic_msg.content.len(), 1);
722
723 match &anthropic_msg.content[0] {
725 AnthropicContentPart::ToolResult(tool_result) => {
726 assert_eq!(tool_result.type_field, "tool_result");
727 assert_eq!(tool_result.tool_call_id, "tool_call_123");
728 assert_eq!(tool_result.content, "The weather is sunny.");
729 }
730 _ => panic!("Expected tool result content"),
731 }
732 }
733
734 #[test]
735 fn test_anthropic_response_to_message() {
736 let response = AnthropicResponse {
738 id: "msg_123".to_string(),
739 type_field: "message".to_string(),
740 role: "assistant".to_string(),
741 model: "claude-3-opus-20240229".to_string(),
742 stop_reason: Some("end_turn".to_string()),
743 content: vec![AnthropicResponseContent::Text {
744 text: "I'm Claude, an AI assistant.".to_string(),
745 }],
746 usage: AnthropicUsage {
747 input_tokens: 10,
748 output_tokens: 20,
749 },
750 };
751
752 let msg = Message::from(&response);
753
754 match &msg {
756 Message::Assistant {
757 content,
758 tool_calls,
759 ..
760 } => {
761 match content {
763 Some(Content::Text(text)) => assert_eq!(text, "I'm Claude, an AI assistant."),
764 _ => panic!("Expected text content"),
765 }
766 assert!(tool_calls.is_empty());
768 }
769 _ => panic!("Expected Assistant variant"),
770 }
771
772 match &msg {
774 Message::Assistant { metadata, .. } => {
775 assert_eq!(metadata["input_tokens"], 10);
776 assert_eq!(metadata["output_tokens"], 20);
777 }
778 _ => panic!("Expected Assistant variant"),
779 }
780
781 let response = AnthropicResponse {
783 id: "msg_123".to_string(),
784 type_field: "message".to_string(),
785 role: "assistant".to_string(),
786 model: "claude-3-opus-20240229".to_string(),
787 stop_reason: Some("end_turn".to_string()),
788 content: vec![
789 AnthropicResponseContent::Text {
790 text: "Here's the information:".to_string(),
791 },
792 AnthropicResponseContent::ToolUse {
793 id: "tool_123".to_string(),
794 name: "get_weather".to_string(),
795 input: serde_json::json!({
796 "location": "San Francisco",
797 }),
798 },
799 ],
800 usage: AnthropicUsage {
801 input_tokens: 15,
802 output_tokens: 30,
803 },
804 };
805
806 let msg = Message::from(&response);
807
808 match &msg {
810 Message::Assistant {
811 content,
812 tool_calls,
813 ..
814 } => {
815 match content {
817 Some(Content::Text(text)) => {
818 assert_eq!(text, "Here's the information:");
819 }
820 Some(Content::Parts(parts)) => {
821 assert_eq!(parts.len(), 1);
822 match &parts[0] {
823 ContentPart::Text { text } => {
824 assert_eq!(text, "Here's the information:")
825 }
826 _ => panic!("Expected text content"),
827 }
828 }
829 _ => panic!("Expected content"),
830 }
831
832 assert_eq!(tool_calls.len(), 1);
834
835 let tool_call = &tool_calls[0];
836 assert_eq!(tool_call.id, "tool_123");
837 assert_eq!(tool_call.tool_type, "function");
838 assert_eq!(tool_call.function.name, "get_weather");
839
840 let arguments: serde_json::Value =
842 serde_json::from_str(&tool_call.function.arguments).unwrap();
843 assert_eq!(arguments["location"], "San Francisco");
844 }
845 _ => panic!("Expected Assistant variant"),
846 }
847 }
848
849 #[test]
850 fn test_anthropic_response_with_tool_use_only() {
851 let response = AnthropicResponse {
853 id: "msg_123".to_string(),
854 type_field: "message".to_string(),
855 role: "assistant".to_string(),
856 model: "claude-3-opus-20240229".to_string(),
857 stop_reason: Some("end_turn".to_string()),
858 content: vec![AnthropicResponseContent::ToolUse {
859 id: "tool_xyz".to_string(),
860 name: "calculate".to_string(),
861 input: serde_json::json!({
862 "expression": "2+2",
863 }),
864 }],
865 usage: AnthropicUsage {
866 input_tokens: 5,
867 output_tokens: 10,
868 },
869 };
870
871 let msg = Message::from(&response);
872
873 match &msg {
875 Message::Assistant {
876 content,
877 tool_calls,
878 ..
879 } => {
880 assert!(
882 content.is_none()
883 || (match content {
884 Some(Content::Parts(parts)) => parts.is_empty(),
885 _ => false,
886 })
887 );
888
889 assert_eq!(tool_calls.len(), 1);
891
892 let tool_call = &tool_calls[0];
893 assert_eq!(tool_call.id, "tool_xyz");
894 assert_eq!(tool_call.function.name, "calculate");
895
896 let arguments: serde_json::Value =
898 serde_json::from_str(&tool_call.function.arguments).unwrap();
899 assert_eq!(arguments["expression"], "2+2");
900 }
901 _ => panic!("Expected Assistant variant"),
902 }
903 }
904
905 #[test]
906 fn test_manual_message_conversion() {
907 let text_msg = Message::user("Hello, world!");
911 let anthropic_msg = AnthropicMessage::from(&text_msg);
912
913 assert_eq!(anthropic_msg.role, "user");
915 assert_eq!(anthropic_msg.content.len(), 1);
916 match &anthropic_msg.content[0] {
917 AnthropicContentPart::Text { text } => assert_eq!(text, "Hello, world!"),
918 _ => panic!("Expected text content"),
919 }
920
921 let response = AnthropicResponse {
923 id: "msg_123".to_string(),
924 type_field: "message".to_string(),
925 role: "assistant".to_string(),
926 model: "claude-3-opus-20240229".to_string(),
927 stop_reason: Some("end_turn".to_string()),
928 content: vec![AnthropicResponseContent::Text {
929 text: "I'm here to help!".to_string(),
930 }],
931 usage: AnthropicUsage {
932 input_tokens: 5,
933 output_tokens: 15,
934 },
935 };
936
937 let lib_msg = Message::from(&response);
939
940 match &lib_msg {
942 Message::Assistant {
943 content,
944 tool_calls,
945 metadata,
946 } => {
947 match content {
949 Some(Content::Text(text)) => assert_eq!(text, "I'm here to help!"),
950 _ => panic!("Expected text content"),
951 }
952
953 assert!(tool_calls.is_empty());
955
956 assert_eq!(metadata["input_tokens"], 5);
958 assert_eq!(metadata["output_tokens"], 15);
959 }
960 _ => panic!("Expected Assistant variant"),
961 }
962 }
963
964 #[test]
965 fn test_headers() {
966 let config = AnthropicConfig {
971 api_key: "test-api-key".to_string(),
972 base_url: "https://api.anthropic.com/v1".to_string(),
973 api_version: "2023-06-01".to_string(),
974 };
975 let provider = AnthropicProvider::with_config(config);
976
977 let model = Claude::Sonnet37 {
979 use_extended_thinking: false,
980 };
981 let chat = Chat::new(model)
982 .with_system_prompt("You are a helpful assistant.")
983 .with_max_output_tokens(1024)
984 .add_message(Message::user("Hello, how are you?"))
985 .add_message(Message::assistant("I'm doing well, thank you for asking!"))
986 .add_message(Message::user("Can you help me with a question?"));
987
988 let request = provider.accept(chat).unwrap();
990
991 assert_eq!(request.method(), "POST");
993 assert_eq!(
994 request.url().as_str(),
995 "https://api.anthropic.com/v1/messages"
996 );
997 assert_eq!(request.headers()["x-api-key"], "test-api-key");
998 assert_eq!(request.headers()["anthropic-version"], "2023-06-01");
999 assert_eq!(request.headers()["Content-Type"], "application/json");
1000 }
1001}