language_barrier_core/provider/
anthropic.rs

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/// Configuration for the Anthropic provider
13#[derive(Debug, Clone)]
14pub struct AnthropicConfig {
15    /// API key for authentication
16    pub api_key: String,
17    /// Base URL for the API
18    pub base_url: String,
19    /// API version header
20    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/// Implementation of the Anthropic provider
34#[derive(Debug, Clone)]
35pub struct AnthropicProvider {
36    /// Configuration for the provider
37    config: AnthropicConfig,
38}
39
40impl AnthropicProvider {
41    /// Creates a new AnthropicProvider with default configuration
42    ///
43    /// This method will use the ANTHROPIC_API_KEY environment variable for authentication.
44    ///
45    /// # Examples
46    ///
47    /// ```
48    /// use language_barrier_core::provider::anthropic::AnthropicProvider;
49    ///
50    /// let provider = AnthropicProvider::new();
51    /// ```
52    #[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    /// Creates a new AnthropicProvider with custom configuration
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use language_barrier_core::provider::anthropic::{AnthropicProvider, AnthropicConfig};
69    ///
70    /// let config = AnthropicConfig {
71    ///     api_key: "your-api-key".to_string(),
72    ///     base_url: "https://api.anthropic.com/v1".to_string(),
73    ///     api_version: "2023-06-01".to_string(),
74    /// };
75    ///
76    /// let provider = AnthropicProvider::with_config(config);
77    /// ```
78    #[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        // Set headers
117        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        // Create the request payload
153        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        // Set the request body
170        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        // First check if it's an error response
193        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        // Convert to our message format using the existing From implementation
238        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    /// Creates a request payload from a Chat object
269    ///
270    /// This method converts the Chat's messages and settings into an Anthropic-specific
271    /// format for the API request.
272    #[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        // Convert system prompt if present
280        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        // Convert messages
290        debug!("Converting messages to Anthropic format");
291        let messages: Vec<AnthropicMessage> = chat
292            .history
293            .iter()
294            .filter(|msg| !matches!(msg, Message::System { .. })) // Filter out system messages as they go in system field
295            .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        // Get model ID for the chat model
304        let model_id = Self::id_for_model(chat.model).to_string();
305        debug!("Using model ID: {}", model_id);
306
307        // Convert tool descriptions if a tool registry is provided
308        let tools = chat
309            .tools
310            .as_ref()
311            .map(|tools| tools.iter().map(AnthropicTool::from).collect());
312
313        // Create the request
314        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/// Represents a message in the Anthropic API format
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub(crate) struct AnthropicMessage {
334    /// The role of the message sender (user or assistant)
335    pub role: String,
336    /// The content of the message
337    pub content: Vec<AnthropicContentPart>,
338}
339
340/// Represents a content part in an Anthropic message
341#[derive(Debug, Clone, Serialize, Deserialize)]
342#[serde(tag = "type")]
343pub(crate) enum AnthropicContentPart {
344    /// Text content
345    #[serde(rename = "text")]
346    Text {
347        /// The text content
348        text: String,
349    },
350    /// Image content
351    #[serde(rename = "image")]
352    Image {
353        /// The source of the image
354        source: AnthropicImageSource,
355    },
356    /// Tool result content (for tool responses)
357    #[serde(rename = "tool_result")]
358    ToolResult(AnthropicToolResponse),
359}
360
361impl AnthropicContentPart {
362    /// Create a new text content part
363    fn text(text: String) -> Self {
364        AnthropicContentPart::Text { text }
365    }
366
367    /// Create a new image content part
368    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/// Represents the source of an image in an Anthropic message
380#[derive(Debug, Clone, Serialize, Deserialize)]
381pub(crate) struct AnthropicImageSource {
382    /// The type of the image source (base64 or url)
383    #[serde(rename = "type")]
384    pub type_field: String,
385    /// The media type of the image
386    pub media_type: String,
387    /// The image data (base64 or url)
388    pub data: String,
389}
390
391/// Represents a tool response in an Anthropic message
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub(crate) struct AnthropicToolResponse {
394    /// The type of the tool response
395    #[serde(rename = "type")]
396    pub type_field: String,
397    /// The ID of the tool call (Anthropic API uses `tool_use_id`, but we use `tool_call_id` internally)
398    #[serde(rename = "tool_use_id")]
399    pub tool_call_id: String,
400    /// The content of the tool response
401    pub content: String,
402}
403
404/// Represents a tool in the Anthropic API format
405#[derive(Debug, Clone, Serialize, Deserialize)]
406pub(crate) struct AnthropicTool {
407    /// The name of the tool
408    pub name: String,
409    /// The description of the tool
410    pub description: String,
411    /// The input schema for the tool
412    pub input_schema: serde_json::Value,
413}
414
415/// Represents a request to the Anthropic API
416#[derive(Debug, Serialize, Deserialize)]
417pub(crate) struct AnthropicRequest {
418    /// The model to use for generation
419    pub model: String,
420    /// The messages to send to the model
421    pub messages: Vec<AnthropicMessage>,
422    /// The system prompt (optional)
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub system: Option<String>,
425    /// The maximum number of tokens to generate
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub max_tokens: Option<usize>,
428    /// The temperature (randomness) of the generation
429    #[serde(skip_serializing_if = "Option::is_none")]
430    pub temperature: Option<f32>,
431    /// The top-p sampling parameter
432    #[serde(skip_serializing_if = "Option::is_none")]
433    pub top_p: Option<f32>,
434    /// The top-k sampling parameter
435    #[serde(skip_serializing_if = "Option::is_none")]
436    pub top_k: Option<u32>,
437    /// The tools available to the model
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub tools: Option<Vec<AnthropicTool>>,
440}
441
442/// Represents a response from the Anthropic API
443#[derive(Debug, Serialize, Deserialize)]
444pub(crate) struct AnthropicResponse {
445    /// The ID of the response
446    pub id: String,
447    /// The type of the response
448    #[serde(rename = "type")]
449    pub type_field: String,
450    /// The role of the message
451    pub role: String,
452    /// The model used for generation
453    pub model: String,
454    /// Whether the response is complete
455    pub stop_reason: Option<String>,
456    /// The content of the response
457    pub content: Vec<AnthropicResponseContent>,
458    /// Usage information
459    pub usage: AnthropicUsage,
460}
461
462/// Represents a content part in an Anthropic response
463#[derive(Debug, Serialize, Deserialize)]
464#[serde(tag = "type")]
465pub(crate) enum AnthropicResponseContent {
466    /// Text content
467    #[serde(rename = "text")]
468    Text {
469        /// The text content
470        text: String,
471    },
472    /// Tool use content
473    #[serde(rename = "tool_use")]
474    ToolUse {
475        /// The ID of the tool use
476        id: String,
477        /// The name of the tool
478        name: String,
479        /// The input to the tool
480        input: serde_json::Value,
481    },
482}
483
484/// Represents usage information in an Anthropic response
485#[derive(Debug, Serialize, Deserialize)]
486pub(crate) struct AnthropicUsage {
487    /// Number of tokens in the input
488    pub input_tokens: u32,
489    /// Number of tokens in the output
490    pub output_tokens: u32,
491}
492
493/// Convert from our Message to Anthropic's message format
494impl 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", // API requires tool_result blocks to be in user messages
499            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                // For tool messages, add a tool_result part
536                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
548/// Convert from Anthropic's response content to our content part
549impl 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                // For tool use, we'll create a text representation
555                let text = format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"input\":{input}}}");
556                ContentPart::text(text)
557            }
558        }
559    }
560}
561
562/// Convert from Anthropic's response to our message format
563impl From<&AnthropicResponse> for Message {
564    fn from(response: &AnthropicResponse) -> Self {
565        // Extract text content and tool calls from response content
566        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                    // Convert tool use to our ToolCall format
576                    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        // Create content based on what we found
593        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        // Create the message based on response.role
605        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            // Default to user for unknown roles
626            _ => 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        // Add token usage info as metadata
638        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        // Test simple text message
670        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        // Test multipart message
681        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        // Test system message
689        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        // Test image content
696        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        // Verify the image content
707        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        // Test tool message
717        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        // Verify tool content
724        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        // Test single text content
737        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        // Check the message is an Assistant variant
755        match &msg {
756            Message::Assistant {
757                content,
758                tool_calls,
759                ..
760            } => {
761                // Verify content
762                match content {
763                    Some(Content::Text(text)) => assert_eq!(text, "I'm Claude, an AI assistant."),
764                    _ => panic!("Expected text content"),
765                }
766                // Verify no tool calls
767                assert!(tool_calls.is_empty());
768            }
769            _ => panic!("Expected Assistant variant"),
770        }
771
772        // Check metadata
773        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        // Test multiple content parts
782        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        // Check the message is an Assistant variant
809        match &msg {
810            Message::Assistant {
811                content,
812                tool_calls,
813                ..
814            } => {
815                // Verify content
816                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                // Verify tool calls
833                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                // Parse the arguments JSON to verify it
841                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        // Test response with only tool use (no text)
852        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        // Check the message is an Assistant variant
874        match &msg {
875            Message::Assistant {
876                content,
877                tool_calls,
878                ..
879            } => {
880                // Content should be None since there's no text
881                assert!(
882                    content.is_none()
883                        || (match content {
884                            Some(Content::Parts(parts)) => parts.is_empty(),
885                            _ => false,
886                        })
887                );
888
889                // Verify tool calls
890                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                // Parse the arguments JSON to verify it
897                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        // Test that we can directly convert between types without relying on Chat
908
909        // Create a message
910        let text_msg = Message::user("Hello, world!");
911        let anthropic_msg = AnthropicMessage::from(&text_msg);
912
913        // Verify the conversion
914        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        // Create a manual Anthropic response
922        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        // Convert to our message type
938        let lib_msg = Message::from(&response);
939
940        // Verify the conversion
941        match &lib_msg {
942            Message::Assistant {
943                content,
944                tool_calls,
945                metadata,
946            } => {
947                // Verify content
948                match content {
949                    Some(Content::Text(text)) => assert_eq!(text, "I'm here to help!"),
950                    _ => panic!("Expected text content"),
951                }
952
953                // Verify no tool calls
954                assert!(tool_calls.is_empty());
955
956                // Check metadata
957                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        // This test may fail due to transitional state in the codebase
967        // We'll implement it properly but comment it out for now
968
969        // Create a provider with a test API key
970        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        // Create a chat with a model
978        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        // Create the request
989        let request = provider.accept(chat).unwrap();
990
991        // Verify the request
992        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}