use std::time::Duration;
use reqwest::{Client, ClientBuilder, Response};
use serde_json::{Value, json};
use tokio::time::timeout;
use crate::core::providers::base::{
HeaderPair, apply_headers, header, header_owned, header_static,
};
use crate::core::providers::shared::parse_retry_after_from_body;
use crate::core::providers::unified_provider::ProviderError;
use crate::core::types::{
chat::ChatMessage,
chat::ChatRequest,
content::ContentPart,
message::MessageRole,
responses::{ChatChoice, ChatResponse, Usage},
};
use super::config::AnthropicConfig;
use super::error::{
anthropic_api_error, anthropic_auth_error, anthropic_network_error, anthropic_parse_error,
anthropic_rate_limit_error,
};
use super::models::{ModelFeature, get_anthropic_registry};
#[derive(Debug, Clone)]
pub struct AnthropicClient {
config: AnthropicConfig,
http_client: Client,
}
impl AnthropicClient {
pub fn new(config: AnthropicConfig) -> Result<Self, ProviderError> {
let mut builder = ClientBuilder::new()
.timeout(Duration::from_secs(config.request_timeout))
.connect_timeout(Duration::from_secs(config.connect_timeout));
if let Some(proxy_url) = &config.proxy_url {
let proxy = reqwest::Proxy::all(proxy_url)
.map_err(|e| anthropic_network_error(format!("Invalid proxy URL: {}", e)))?;
builder = builder.proxy(proxy);
}
let http_client = builder
.build()
.map_err(|e| anthropic_network_error(format!("Failed to create HTTP client: {}", e)))?;
Ok(Self {
config,
http_client,
})
}
pub async fn chat(&self, request: ChatRequest) -> Result<ChatResponse, ProviderError> {
let anthropic_request = self.transform_chat_request(&request)?;
let mut headers = self.get_request_headers();
headers.extend(self.compute_beta_headers(&request));
let response = self
.send_request("/v1/messages", anthropic_request, headers)
.await?;
self.transform_chat_response(response)
}
pub async fn chat_stream(
&self,
request: ChatRequest,
) -> Result<reqwest::Response, ProviderError> {
let mut anthropic_request = self.transform_chat_request(&request)?;
anthropic_request["stream"] = json!(true);
let mut headers = self.get_request_headers();
headers.extend(self.compute_beta_headers(&request));
self.send_stream_request("/v1/messages", anthropic_request, headers)
.await
}
async fn send_request(
&self,
endpoint: &str,
body: Value,
headers: Vec<HeaderPair>,
) -> Result<Value, ProviderError> {
let url = format!("{}{}", self.config.base_url.trim_end_matches('/'), endpoint);
let response = timeout(
Duration::from_secs(self.config.request_timeout),
apply_headers(self.http_client.post(&url).json(&body), headers).send(),
)
.await
.map_err(|_| anthropic_network_error("Request timeout"))?
.map_err(|e| anthropic_network_error(format!("Network error: {}", e)))?;
self.handle_response(response).await
}
async fn send_stream_request(
&self,
endpoint: &str,
body: Value,
headers: Vec<HeaderPair>,
) -> Result<Response, ProviderError> {
let url = format!("{}{}", self.config.base_url.trim_end_matches('/'), endpoint);
let response = timeout(
Duration::from_secs(self.config.request_timeout),
apply_headers(self.http_client.post(&url).json(&body), headers).send(),
)
.await
.map_err(|_| anthropic_network_error("Request timeout"))?
.map_err(|e| anthropic_network_error(format!("Network error: {}", e)))?;
if !response.status().is_success() {
let status = response.status().as_u16();
let error_text = response
.text()
.await
.unwrap_or_else(|_| "Failed to read error response".to_string());
return Err(self.map_http_error(status, &error_text));
}
Ok(response)
}
pub fn get_request_headers(&self) -> Vec<HeaderPair> {
let mut headers = Vec::with_capacity(5);
if let Some(ref api_key) = self.config.api_key {
headers.push(header("x-api-key", api_key.clone()));
}
headers.push(header("anthropic-version", self.config.api_version.clone()));
headers.push(header_static("Content-Type", "application/json"));
headers.push(header_static("User-Agent", "LiteLLM-Rust/1.0"));
for (key, value) in &self.config.custom_headers {
headers.push(header_owned(key.clone(), value.clone()));
}
headers
}
fn compute_beta_headers(&self, request: &ChatRequest) -> Vec<HeaderPair> {
let mut features: Vec<String> = Vec::new();
if request.thinking.as_ref().is_some_and(|t| t.enabled) {
features.push("interleaved-thinking-2025-05-14".to_string());
}
if let Some(arr) = request
.extra_params
.get("anthropic_tools")
.and_then(|v| v.as_array())
{
for tool in arr {
if tool.get("type").and_then(|t| t.as_str()) == Some("computer_20241022") {
features.push("computer-use-2024-10-22".to_string());
break;
}
}
}
if let Some(beta_val) = request.extra_params.get("anthropic_beta") {
match beta_val {
Value::Array(arr) => {
for item in arr {
if let Some(s) = item.as_str() {
let s = s.to_string();
if !features.contains(&s) {
features.push(s);
}
}
}
}
Value::String(s) => {
if !features.contains(s) {
features.push(s.clone());
}
}
_ => {}
}
}
if features.is_empty() {
return vec![];
}
vec![header("anthropic-beta", features.join(","))]
}
async fn handle_response(&self, response: Response) -> Result<Value, ProviderError> {
let status = response.status().as_u16();
let response_text = response
.text()
.await
.map_err(|e| anthropic_network_error(format!("Failed to read response: {}", e)))?;
if status != 200 {
return Err(self.map_http_error(status, &response_text));
}
serde_json::from_str(&response_text)
.map_err(|e| anthropic_parse_error(format!("Failed to parse JSON: {}", e)))
}
fn map_http_error(&self, status: u16, body: &str) -> ProviderError {
match status {
400 => anthropic_api_error(400, format!("Bad request: {}", body)),
401 => anthropic_auth_error("Invalid or missing API key"),
403 => anthropic_auth_error("Forbidden: insufficient permissions"),
404 => anthropic_api_error(404, "Model or endpoint not found"),
429 => {
let retry_after = parse_retry_after_from_body(body);
anthropic_rate_limit_error(retry_after)
}
500..=599 => anthropic_api_error(status, format!("Server error: {}", body)),
_ => anthropic_api_error(status, body),
}
}
fn transform_chat_request(&self, request: &ChatRequest) -> Result<Value, ProviderError> {
let registry = get_anthropic_registry();
let model_spec = registry.get_model_spec(&request.model).ok_or_else(|| {
anthropic_api_error(400, format!("Unsupported model: {}", request.model))
})?;
let (system_message, messages) = self.separate_system_messages(&request.messages)?;
let anthropic_messages = self.transform_messages(messages, model_spec)?;
let mut anthropic_request = json!({
"model": request.model,
"max_tokens": request.max_tokens.unwrap_or(4096),
"messages": anthropic_messages,
});
if let Some(system) = system_message {
anthropic_request["system"] = json!(system);
}
if let Some(temperature) = request.temperature {
anthropic_request["temperature"] = json!(temperature);
}
if let Some(top_p) = request.top_p {
anthropic_request["top_p"] = json!(top_p);
}
if let Some(stop) = &request.stop {
anthropic_request["stop_sequences"] = json!(stop);
}
if let Some(tools) = &request.tools
&& model_spec.features.contains(&ModelFeature::ToolCalling)
{
let anthropic_tools = self.transform_tools(tools)?;
anthropic_request["tools"] = json!(anthropic_tools);
if let Some(tool_choice) = &request.tool_choice {
anthropic_request["tool_choice"] = self.transform_tool_choice(tool_choice)?;
}
}
if let Some(thinking) = &request.thinking
&& thinking.enabled
&& model_spec.features.contains(&ModelFeature::ThinkingMode)
{
let budget = thinking.budget_tokens.unwrap_or(10_000);
let current_max = request.max_tokens.unwrap_or(4096);
if current_max <= budget {
anthropic_request["max_tokens"] = json!(budget + 1);
}
anthropic_request["thinking"] = json!({
"type": "enabled",
"budget_tokens": budget
});
}
if let Some(rf) = &request.response_format
&& rf.format_type == "json_schema"
&& let Some(schema) = &rf.json_schema
{
anthropic_request["response_format"] = json!({
"type": "json_schema",
"json_schema": schema
});
}
if let Some(arr) = request
.extra_params
.get("anthropic_tools")
.and_then(|v| v.as_array())
.filter(|a| !a.is_empty())
{
let mut merged: Vec<Value> = anthropic_request
.get("tools")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
merged.extend(arr.iter().cloned());
anthropic_request["tools"] = json!(merged);
}
Ok(anthropic_request)
}
fn separate_system_messages(
&self,
messages: &[ChatMessage],
) -> Result<(Option<String>, Vec<ChatMessage>), ProviderError> {
let mut system_parts = Vec::new();
let mut user_messages = Vec::new();
for message in messages {
match message.role {
MessageRole::System | MessageRole::Developer => {
if let Some(content) = &message.content {
match content {
crate::core::types::message::MessageContent::Text(text) => {
system_parts.push(text.clone());
}
crate::core::types::message::MessageContent::Parts(parts) => {
for part in parts {
if let ContentPart::Text { text } = part {
system_parts.push(text.clone());
}
}
}
}
}
}
_ => {
user_messages.push(message.clone());
}
}
}
let system_message = if system_parts.is_empty() {
None
} else {
Some(system_parts.join("\n"))
};
Ok((system_message, user_messages))
}
fn transform_messages(
&self,
messages: Vec<ChatMessage>,
model_spec: &super::models::ModelSpec,
) -> Result<Vec<Value>, ProviderError> {
let mut anthropic_messages = Vec::new();
for message in messages {
let role = match message.role {
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::Tool => "user", MessageRole::Function => "user", MessageRole::System | MessageRole::Developer => continue, };
let content = if let Some(content) = message.content {
match content {
crate::core::types::message::MessageContent::Text(text) => {
json!(text)
}
crate::core::types::message::MessageContent::Parts(parts) => {
let mut anthropic_parts = Vec::new();
for part in parts {
match part {
ContentPart::Text { text } => {
anthropic_parts.push(json!({
"type": "text",
"text": text
}));
}
ContentPart::ImageUrl { image_url } => {
if model_spec
.features
.contains(&ModelFeature::MultimodalSupport)
{
if image_url.url.starts_with("data:") {
let parts: Vec<&str> =
image_url.url.split(',').collect();
if parts.len() == 2 {
let media_type = parts[0]
.strip_prefix("data:")
.and_then(|s| s.split(';').next())
.unwrap_or("image/jpeg");
anthropic_parts.push(json!({
"type": "image",
"source": {
"type": "base64",
"media_type": media_type,
"data": parts[1]
}
}));
}
} else {
return Err(anthropic_api_error(
400,
"URL images not yet supported, use base64 format",
));
}
}
}
ContentPart::Document { source, .. } => {
if model_spec
.features
.contains(&ModelFeature::MultimodalSupport)
{
anthropic_parts.push(json!({
"type": "document",
"source": {
"type": "base64",
"media_type": source.media_type,
"data": source.data
}
}));
}
}
_ => {
}
}
}
json!(anthropic_parts)
}
}
} else {
json!("")
};
let mut anthropic_message = json!({
"role": role,
"content": content
});
if let Some(tool_calls) = &message.tool_calls {
let mut anthropic_tool_calls = Vec::new();
for tool_call in tool_calls {
anthropic_tool_calls.push(json!({
"type": "tool_use",
"id": tool_call.id,
"name": tool_call.function.name,
"input": serde_json::from_str::<Value>(&tool_call.function.arguments)
.unwrap_or(json!({}))
}));
}
anthropic_message["content"] = json!(anthropic_tool_calls);
}
anthropic_messages.push(anthropic_message);
}
Ok(anthropic_messages)
}
fn transform_tools(
&self,
tools: &[crate::core::types::tools::Tool],
) -> Result<Vec<Value>, ProviderError> {
let mut anthropic_tools = Vec::new();
for tool in tools {
anthropic_tools.push(json!({
"name": tool.function.name,
"description": tool.function.description.as_ref().unwrap_or(&String::new()),
"input_schema": tool.function.parameters.as_ref().unwrap_or(&json!({}))
}));
}
Ok(anthropic_tools)
}
fn transform_tool_choice(
&self,
tool_choice: &crate::core::types::tools::ToolChoice,
) -> Result<Value, ProviderError> {
match tool_choice {
crate::core::types::tools::ToolChoice::String(choice) => match choice.as_str() {
"auto" => Ok(json!({"type": "auto"})),
"none" => Ok(json!({"type": "none"})),
"required" => Ok(json!({"type": "any"})),
_ => Ok(json!({"type": "auto"})),
},
crate::core::types::tools::ToolChoice::Specific { function, .. } => {
if let Some(func) = function {
Ok(json!({
"type": "tool",
"name": func.name
}))
} else {
Ok(json!({"type": "auto"}))
}
}
}
}
fn transform_chat_response(&self, response: Value) -> Result<ChatResponse, ProviderError> {
let id = response
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let model = response
.get("model")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let created = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let content = response
.get("content")
.and_then(|v| v.as_array())
.ok_or_else(|| anthropic_parse_error("Missing or invalid content array"))?;
let mut message_content = String::new();
let mut tool_calls = Vec::new();
for item in content {
match item.get("type").and_then(|t| t.as_str()) {
Some("text") => {
if let Some(text) = item.get("text").and_then(|t| t.as_str()) {
message_content.push_str(text);
}
}
Some("tool_use") => {
if let (Some(id), Some(name), Some(input)) = (
item.get("id").and_then(|v| v.as_str()),
item.get("name").and_then(|v| v.as_str()),
item.get("input"),
) {
tool_calls.push(crate::core::types::tools::ToolCall {
id: id.to_string(),
tool_type: "function".to_string(),
function: crate::core::types::tools::FunctionCall {
name: name.to_string(),
arguments: input.to_string(),
},
});
}
}
_ => {}
}
}
let message = ChatMessage {
role: MessageRole::Assistant,
content: if message_content.is_empty() {
None
} else {
Some(crate::core::types::message::MessageContent::Text(
message_content,
))
},
thinking: None,
name: None,
tool_calls: if tool_calls.is_empty() {
None
} else {
Some(tool_calls)
},
tool_call_id: None,
function_call: None,
};
let choice = ChatChoice {
index: 0,
message,
finish_reason: response
.get("stop_reason")
.and_then(|r| r.as_str())
.map(|reason| match reason {
"end_turn" => crate::core::types::responses::FinishReason::Stop,
"max_tokens" => crate::core::types::responses::FinishReason::Length,
"tool_use" => crate::core::types::responses::FinishReason::ToolCalls,
_ => crate::core::types::responses::FinishReason::Stop,
}),
logprobs: None,
};
let usage = response.get("usage").map(|usage_data| Usage {
prompt_tokens: usage_data
.get("input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32,
completion_tokens: usage_data
.get("output_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0) as u32,
total_tokens: (usage_data
.get("input_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0)
+ usage_data
.get("output_tokens")
.and_then(|v| v.as_u64())
.unwrap_or(0)) as u32,
completion_tokens_details: None,
prompt_tokens_details: None,
thinking_usage: None,
});
Ok(ChatResponse {
id,
object: "chat.completion".to_string(),
created,
model,
choices: vec![choice],
usage,
system_fingerprint: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::providers::anthropic::config::AnthropicConfig;
use crate::core::types::message::MessageContent;
#[test]
fn test_client_creation() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config);
assert!(client.is_ok());
}
#[test]
fn test_client_creation_with_custom_config() {
let mut config = AnthropicConfig::new_test("test-key");
config.request_timeout = 120;
config.connect_timeout = 30;
let client = AnthropicClient::new(config);
assert!(client.is_ok());
}
fn has_header(headers: &[HeaderPair], key: &str) -> bool {
headers.iter().any(|(k, _)| k.eq_ignore_ascii_case(key))
}
fn get_header<'a>(headers: &'a [HeaderPair], key: &str) -> Option<&'a str> {
headers
.iter()
.find(|(k, _)| k.eq_ignore_ascii_case(key))
.map(|(_, v)| v.as_ref())
}
#[test]
fn test_header_building() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let headers = client.get_request_headers();
assert!(has_header(&headers, "x-api-key"));
assert!(has_header(&headers, "anthropic-version"));
assert!(has_header(&headers, "Content-Type"));
assert!(has_header(&headers, "User-Agent"));
}
#[test]
fn test_header_content_type() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let headers = client.get_request_headers();
assert_eq!(
get_header(&headers, "Content-Type").unwrap(),
"application/json"
);
}
#[test]
fn test_header_user_agent() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let headers = client.get_request_headers();
assert_eq!(
get_header(&headers, "User-Agent").unwrap(),
"LiteLLM-Rust/1.0"
);
}
#[test]
fn test_header_with_custom_headers() {
let mut config = AnthropicConfig::new_test("test-key");
config
.custom_headers
.insert("X-Custom-Header".to_string(), "custom-value".to_string());
let client = AnthropicClient::new(config).unwrap();
let headers = client.get_request_headers();
assert!(has_header(&headers, "X-Custom-Header"));
}
#[test]
fn test_map_http_error_400() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let error = client.map_http_error(400, "invalid request");
let error_string = format!("{}", error);
assert!(error_string.contains("400") || error_string.contains("request"));
}
#[test]
fn test_map_http_error_401() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let error = client.map_http_error(401, "unauthorized");
let error_string = format!("{}", error);
assert!(error_string.to_lowercase().contains("auth") || error_string.contains("key"));
}
#[test]
fn test_map_http_error_403() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let error = client.map_http_error(403, "forbidden");
let error_string = format!("{}", error);
assert!(
error_string.to_lowercase().contains("forbidden")
|| error_string.to_lowercase().contains("permission")
|| error_string.to_lowercase().contains("auth")
);
}
#[test]
fn test_map_http_error_404() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let error = client.map_http_error(404, "not found");
let error_string = format!("{}", error);
assert!(error_string.contains("404") || error_string.contains("not found"));
}
#[test]
fn test_map_http_error_429() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let error = client.map_http_error(429, "rate limited");
let error_string = format!("{}", error);
assert!(
error_string.to_lowercase().contains("rate")
|| error_string.to_lowercase().contains("limit")
);
}
#[test]
fn test_map_http_error_500() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let error = client.map_http_error(500, "server error");
let error_string = format!("{}", error);
assert!(error_string.contains("500") || error_string.to_lowercase().contains("server"));
}
#[test]
fn test_extract_retry_after_from_root() {
let body = r#"{"retry_after": 60}"#;
let retry = parse_retry_after_from_body(body);
assert_eq!(retry, Some(60));
}
#[test]
fn test_extract_retry_after_from_error() {
let body = r#"{"error": {"retry_after": 30}}"#;
let retry = parse_retry_after_from_body(body);
assert_eq!(retry, Some(30));
}
#[test]
fn test_extract_retry_after_missing() {
let body = r#"{"message": "no retry info"}"#;
let retry = parse_retry_after_from_body(body);
assert!(retry.is_none());
}
#[test]
fn test_extract_retry_after_invalid_json() {
let body = "not json";
let retry = parse_retry_after_from_body(body);
assert!(retry.is_none());
}
#[test]
fn test_separate_system_messages_no_system() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let messages = vec![ChatMessage {
role: MessageRole::User,
content: Some(MessageContent::Text("Hello".to_string())),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
thinking: None,
}];
let (system, user_msgs) = client.separate_system_messages(&messages).unwrap();
assert!(system.is_none());
assert_eq!(user_msgs.len(), 1);
}
#[test]
fn test_separate_system_messages_with_system() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let messages = vec![
ChatMessage {
role: MessageRole::System,
content: Some(MessageContent::Text(
"You are a helpful assistant.".to_string(),
)),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
thinking: None,
},
ChatMessage {
role: MessageRole::User,
content: Some(MessageContent::Text("Hello".to_string())),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
thinking: None,
},
];
let (system, user_msgs) = client.separate_system_messages(&messages).unwrap();
assert_eq!(system, Some("You are a helpful assistant.".to_string()));
assert_eq!(user_msgs.len(), 1);
}
#[test]
fn test_separate_system_messages_multiple_system() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let messages = vec![
ChatMessage {
role: MessageRole::System,
content: Some(MessageContent::Text("Rule 1".to_string())),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
thinking: None,
},
ChatMessage {
role: MessageRole::System,
content: Some(MessageContent::Text("Rule 2".to_string())),
name: None,
tool_calls: None,
tool_call_id: None,
function_call: None,
thinking: None,
},
];
let (system, _) = client.separate_system_messages(&messages).unwrap();
assert_eq!(system, Some("Rule 1\nRule 2".to_string()));
}
#[test]
fn test_transform_tool_choice_auto() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let tool_choice = crate::core::types::tools::ToolChoice::String("auto".to_string());
let result = client.transform_tool_choice(&tool_choice).unwrap();
assert_eq!(result["type"], "auto");
}
#[test]
fn test_transform_tool_choice_none() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let tool_choice = crate::core::types::tools::ToolChoice::String("none".to_string());
let result = client.transform_tool_choice(&tool_choice).unwrap();
assert_eq!(result["type"], "none");
}
#[test]
fn test_transform_tool_choice_required() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let tool_choice = crate::core::types::tools::ToolChoice::String("required".to_string());
let result = client.transform_tool_choice(&tool_choice).unwrap();
assert_eq!(result["type"], "any");
}
#[test]
fn test_transform_tools() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let tools = vec![crate::core::types::tools::Tool {
tool_type: crate::core::types::tools::ToolType::Function,
function: crate::core::types::tools::FunctionDefinition {
name: "get_weather".to_string(),
description: Some("Get weather for a location".to_string()),
parameters: Some(json!({"type": "object"})),
},
}];
let result = client.transform_tools(&tools).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0]["name"], "get_weather");
assert_eq!(result[0]["description"], "Get weather for a location");
}
#[test]
fn test_transform_tools_empty() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let tools: Vec<crate::core::types::tools::Tool> = vec![];
let result = client.transform_tools(&tools).unwrap();
assert!(result.is_empty());
}
#[test]
fn test_transform_chat_response_text() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let response = json!({
"id": "msg_123",
"model": "claude-3-opus-20240229",
"content": [
{"type": "text", "text": "Hello, world!"}
],
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
});
let result = client.transform_chat_response(response).unwrap();
assert_eq!(result.id, "msg_123");
assert_eq!(result.model, "claude-3-opus-20240229");
assert_eq!(result.choices.len(), 1);
if let Some(MessageContent::Text(text)) = &result.choices.first().unwrap().message.content {
assert_eq!(text, "Hello, world!");
} else {
panic!("Expected text content");
}
}
#[test]
fn test_transform_chat_response_usage() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let response = json!({
"id": "msg_123",
"model": "claude-3-opus-20240229",
"content": [{"type": "text", "text": "Hi"}],
"usage": {
"input_tokens": 100,
"output_tokens": 50
}
});
let result = client.transform_chat_response(response).unwrap();
let usage = result.usage.unwrap();
assert_eq!(usage.prompt_tokens, 100);
assert_eq!(usage.completion_tokens, 50);
assert_eq!(usage.total_tokens, 150);
}
#[test]
fn test_transform_chat_response_tool_use() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let response = json!({
"id": "msg_123",
"model": "claude-3-opus-20240229",
"content": [
{
"type": "tool_use",
"id": "tool_1",
"name": "get_weather",
"input": {"location": "San Francisco"}
}
],
"stop_reason": "tool_use"
});
let result = client.transform_chat_response(response).unwrap();
let tool_calls = result
.choices
.first()
.unwrap()
.message
.tool_calls
.as_ref()
.unwrap();
assert_eq!(tool_calls.len(), 1);
assert_eq!(tool_calls.first().unwrap().id, "tool_1");
assert_eq!(tool_calls.first().unwrap().function.name, "get_weather");
}
#[test]
fn test_transform_chat_response_finish_reasons() {
let config = AnthropicConfig::new_test("test-key");
let client = AnthropicClient::new(config).unwrap();
let response = json!({
"id": "msg_123",
"model": "claude-3-opus-20240229",
"content": [{"type": "text", "text": "Hi"}],
"stop_reason": "end_turn"
});
let result = client.transform_chat_response(response).unwrap();
assert!(matches!(
result.choices.first().unwrap().finish_reason,
Some(crate::core::types::responses::FinishReason::Stop)
));
let response = json!({
"id": "msg_123",
"model": "claude-3-opus-20240229",
"content": [{"type": "text", "text": "Hi"}],
"stop_reason": "max_tokens"
});
let result = client.transform_chat_response(response).unwrap();
assert!(matches!(
result.choices.first().unwrap().finish_reason,
Some(crate::core::types::responses::FinishReason::Length)
));
let response = json!({
"id": "msg_123",
"model": "claude-3-opus-20240229",
"content": [{"type": "text", "text": "Hi"}],
"stop_reason": "tool_use"
});
let result = client.transform_chat_response(response).unwrap();
assert!(matches!(
result.choices.first().unwrap().finish_reason,
Some(crate::core::types::responses::FinishReason::ToolCalls)
));
}
}