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, 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        // 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(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        // 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, 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        // 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(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        debug!("Tools configured: {:?}", tools);
313
314        // Note: For Anthropic, tool_choice is handled through the prompt and context
315        // We don't modify the tools list based on the choice
316
317        // Convert tool_choice to Anthropic's format
318        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                // Anthropic uses "any" instead of "required" for what we call "Any"
326                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                    // For specific tool, use this format
336                    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            // Default to auto if tools are present but no choice specified
347            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        // Create the request
357        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/// Represents a message in the Anthropic API format
376#[derive(Debug, Clone, Serialize, Deserialize)]
377pub(crate) struct AnthropicMessage {
378    /// The role of the message sender (user or assistant)
379    pub role: String,
380    /// The content of the message
381    pub content: Vec<AnthropicContentPart>,
382}
383
384/// Represents a content part in an Anthropic message
385#[derive(Debug, Clone, Serialize, Deserialize)]
386#[serde(tag = "type")]
387pub(crate) enum AnthropicContentPart {
388    /// Text content
389    #[serde(rename = "text")]
390    Text {
391        /// The text content
392        text: String,
393    },
394    /// Image content
395    #[serde(rename = "image")]
396    Image {
397        /// The source of the image
398        source: AnthropicImageSource,
399    },
400    /// Tool result content (for tool responses)
401    #[serde(rename = "tool_result")]
402    ToolResult(AnthropicToolResponse),
403    /// Tool use content (for tool calls initiated by the assistant)
404    #[serde(rename = "tool_use")]
405    ToolUse {
406        /// The ID of the tool use/call
407        id: String,
408        /// Name of the function/tool being invoked
409        name: String,
410        /// Parsed JSON arguments passed to the tool
411        input: serde_json::Value,
412    },
413}
414
415impl AnthropicContentPart {
416    /// Create a new text content part
417    fn text(text: String) -> Self {
418        AnthropicContentPart::Text { text }
419    }
420
421    /// Create a new image content part
422    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/// Represents the source of an image in an Anthropic message
434#[derive(Debug, Clone, Serialize, Deserialize)]
435pub(crate) struct AnthropicImageSource {
436    /// The type of the image source (base64 or url)
437    #[serde(rename = "type")]
438    pub type_field: String,
439    /// The media type of the image
440    pub media_type: String,
441    /// The image data (base64 or url)
442    pub data: String,
443}
444
445/// Represents a tool response in an Anthropic message
446#[derive(Debug, Clone, Serialize, Deserialize)]
447pub(crate) struct AnthropicToolResponse {
448    /// The type of the tool response
449    #[serde(rename = "type")]
450    pub type_field: String,
451    /// The ID of the tool call (Anthropic API uses `tool_use_id`, but we use `tool_call_id` internally)
452    #[serde(rename = "tool_use_id")]
453    pub tool_call_id: String,
454    /// The content of the tool response
455    pub content: String,
456}
457
458/// Represents a tool in the Anthropic API format
459#[derive(Debug, Clone, Serialize, Deserialize)]
460pub(crate) struct AnthropicTool {
461    /// The name of the tool
462    pub name: String,
463    /// The description of the tool
464    pub description: String,
465    /// The input schema for the tool
466    pub input_schema: serde_json::Value,
467}
468
469/// Represents a request to the Anthropic API
470#[derive(Debug, Serialize, Deserialize)]
471pub(crate) struct AnthropicRequest {
472    /// The model to use for generation
473    pub model: String,
474    /// The messages to send to the model
475    pub messages: Vec<AnthropicMessage>,
476    /// The system prompt (optional)
477    #[serde(skip_serializing_if = "Option::is_none")]
478    pub system: Option<String>,
479    /// The maximum number of tokens to generate
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub max_tokens: Option<usize>,
482    /// The temperature (randomness) of the generation
483    #[serde(skip_serializing_if = "Option::is_none")]
484    pub temperature: Option<f32>,
485    /// The top-p sampling parameter
486    #[serde(skip_serializing_if = "Option::is_none")]
487    pub top_p: Option<f32>,
488    /// The top-k sampling parameter
489    #[serde(skip_serializing_if = "Option::is_none")]
490    pub top_k: Option<u32>,
491    /// The tools available to the model
492    #[serde(skip_serializing_if = "Option::is_none")]
493    pub tools: Option<Vec<AnthropicTool>>,
494    /// Tool choice mode
495    #[serde(skip_serializing_if = "Option::is_none")]
496    pub tool_choice: Option<serde_json::Value>,
497}
498
499/// Represents a response from the Anthropic API
500#[derive(Debug, Serialize, Deserialize)]
501pub(crate) struct AnthropicResponse {
502    /// The ID of the response
503    pub id: String,
504    /// The type of the response
505    #[serde(rename = "type")]
506    pub type_field: String,
507    /// The role of the message
508    pub role: String,
509    /// The model used for generation
510    pub model: String,
511    /// Whether the response is complete
512    pub stop_reason: Option<String>,
513    /// The content of the response
514    pub content: Vec<AnthropicResponseContent>,
515    /// Usage information
516    pub usage: AnthropicUsage,
517}
518
519/// Represents a content part in an Anthropic response
520#[derive(Debug, Serialize, Deserialize)]
521#[serde(tag = "type")]
522pub(crate) enum AnthropicResponseContent {
523    /// Text content
524    #[serde(rename = "text")]
525    Text {
526        /// The text content
527        text: String,
528    },
529    /// Tool use content
530    #[serde(rename = "tool_use")]
531    ToolUse {
532        /// The ID of the tool use
533        id: String,
534        /// The name of the tool
535        name: String,
536        /// The input to the tool
537        input: serde_json::Value,
538    },
539}
540
541/// Represents usage information in an Anthropic response
542#[derive(Debug, Serialize, Deserialize)]
543pub(crate) struct AnthropicUsage {
544    /// Number of tokens in the input
545    pub input_tokens: u32,
546    /// Number of tokens in the output
547    pub output_tokens: u32,
548}
549
550/// Convert from our Message to Anthropic's message format
551impl 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", // API requires tool_result blocks to be in user messages
556            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                // Start with any textual or multimodal content parts, if provided
580                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                // Add any tool_use blocks corresponding to tool calls
595                for call in tool_calls {
596                    let parsed_args: serde_json::Value =
597                        serde_json::from_str(&call.function.arguments).unwrap_or_else(|_| {
598                            // Fallback to a raw string if JSON parsing fails
599                            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 no parts were generated (unlikely but possible), create a
610                // single text part with some minimal content to satisfy the API.
611                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                // For tool messages, add a tool_result part
623                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
635/// Convert from Anthropic's response content to our content part
636impl 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                // For tool use, we'll create a text representation
642                let text = format!("{{\"id\":\"{id}\",\"name\":\"{name}\",\"input\":{input}}}");
643                ContentPart::text(text)
644            }
645        }
646    }
647}
648
649/// Convert from Anthropic's response to our message format
650impl From<&AnthropicResponse> for Message {
651    fn from(response: &AnthropicResponse) -> Self {
652        // Extract text content and tool calls from response content
653        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                    // Convert tool use to our ToolCall format
663                    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        // Create content based on what we found
680        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        // Create the message based on response.role
692        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            // Default to user for unknown roles
713            _ => 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        // Add token usage info as metadata
725        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        // Test simple text message
757        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        // Test multipart message
768        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        // Test system message
776        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        // Test image content
783        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        // Verify the image content
794        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        // Test tool message
804        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        // Verify tool content
811        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        // Test single text content
824        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        // Check the message is an Assistant variant
842        match &msg {
843            Message::Assistant {
844                content,
845                tool_calls,
846                ..
847            } => {
848                // Verify content
849                match content {
850                    Some(Content::Text(text)) => assert_eq!(text, "I'm Claude, an AI assistant."),
851                    _ => panic!("Expected text content"),
852                }
853                // Verify no tool calls
854                assert!(tool_calls.is_empty());
855            }
856            _ => panic!("Expected Assistant variant"),
857        }
858
859        // Check metadata
860        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        // Test multiple content parts
869        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        // Check the message is an Assistant variant
896        match &msg {
897            Message::Assistant {
898                content,
899                tool_calls,
900                ..
901            } => {
902                // Verify content
903                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                // Verify tool calls
920                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                // Parse the arguments JSON to verify it
928                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        // Test response with only tool use (no text)
939        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        // Check the message is an Assistant variant
961        match &msg {
962            Message::Assistant {
963                content,
964                tool_calls,
965                ..
966            } => {
967                // Content should be None since there's no text
968                assert!(
969                    content.is_none()
970                        || (match content {
971                            Some(Content::Parts(parts)) => parts.is_empty(),
972                            _ => false,
973                        })
974                );
975
976                // Verify tool calls
977                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                // Parse the arguments JSON to verify it
984                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        // Test that we can directly convert between types without relying on Chat
995
996        // Create a message
997        let text_msg = Message::user("Hello, world!");
998        let anthropic_msg = AnthropicMessage::from(&text_msg);
999
1000        // Verify the conversion
1001        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        // Create a manual Anthropic response
1009        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        // Convert to our message type
1025        let lib_msg = Message::from(&response);
1026
1027        // Verify the conversion
1028        match &lib_msg {
1029            Message::Assistant {
1030                content,
1031                tool_calls,
1032                metadata,
1033            } => {
1034                // Verify content
1035                match content {
1036                    Some(Content::Text(text)) => assert_eq!(text, "I'm here to help!"),
1037                    _ => panic!("Expected text content"),
1038                }
1039
1040                // Verify no tool calls
1041                assert!(tool_calls.is_empty());
1042
1043                // Check metadata
1044                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        // Build an assistant message that has no textual content but contains a tool call
1054        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        // Convert to AnthropicMessage
1068        let anthropic_msg = AnthropicMessage::from(&assistant_msg);
1069
1070        // The assistant role should remain assistant
1071        assert_eq!(anthropic_msg.role, "assistant");
1072
1073        // There should be exactly one content block corresponding to the tool use
1074        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                // Parse the JSON arguments to compare
1082                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        // This test may fail due to transitional state in the codebase
1093        // We'll implement it properly but comment it out for now
1094
1095        // Create a provider with a test API key
1096        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        // Create a chat with a model
1104        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        // Create the request
1115        let request = provider.accept(model, &chat).unwrap();
1116
1117        // Verify the request
1118        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}