use super::*;
use crate::http::{build_request, create_sse_stream, handle_error_response};
use crate::protocol::AuthMethod;
use crate::types::{
CacheControl, CompletionRequest, ContentPart, FinishReason, Message, MessageContent,
ReasoningEffort, StreamEvent, ToolCall, ToolChoice, ToolDefinition,
};
use futures::StreamExt;
use reqwest::header::HeaderMap;
use serde_json::json;
use std::time::Duration;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[test]
fn test_build_body_basic_text() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hello!")]);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["model"], "claude-3-5-sonnet");
assert_eq!(body["max_tokens"], 4096);
assert_eq!(body["messages"][0]["role"], "user");
assert_eq!(body["messages"][0]["content"][0]["type"], "text");
assert_eq!(body["messages"][0]["content"][0]["text"], "Hello!");
assert!(body.get("stream").is_none());
}
#[test]
fn test_build_body_system_text_as_string() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::system("You are a helpful assistant."), Message::user("Hi!")],
);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["system"], "You are a helpful assistant.");
}
#[test]
fn test_build_body_system_with_cache_control_as_array() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest {
model: Some("claude-3-5-sonnet".to_owned()),
messages: vec![
Message::System {
content: MessageContent::Text("Long system prompt".to_owned()),
cache_control: Some(CacheControl::Ephemeral),
},
Message::user("Hi"),
],
..CompletionRequest::default()
};
let body = adapter.build_request_body(&req, false).unwrap();
let system = body["system"].as_array().unwrap();
assert_eq!(system[0]["type"], "text");
assert_eq!(system[0]["text"], "Long system prompt");
assert_eq!(system[0]["cache_control"]["type"], "ephemeral");
}
#[test]
fn test_build_body_tool_use_in_assistant() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![
Message::user("What's the weather?"),
Message::Assistant {
content: MessageContent::Text("Let me check.".to_owned()),
tool_calls: Some(vec![ToolCall {
id: "call_123".to_owned(),
function_name: "get_weather".to_owned(),
arguments: json!({"city": "Tokyo"}),
}]),
cache_control: None,
reasoning_content: None,
},
],
);
let body = adapter.build_request_body(&req, false).unwrap();
let msg = &body["messages"][1];
assert_eq!(msg["role"], "assistant");
assert_eq!(msg["content"][0]["type"], "tool_use");
assert_eq!(msg["content"][0]["id"], "call_123");
assert_eq!(msg["content"][0]["name"], "get_weather");
assert_eq!(msg["content"][0]["input"]["city"], "Tokyo");
assert_eq!(msg["content"][1]["type"], "text");
assert_eq!(msg["content"][1]["text"], "Let me check.");
}
#[test]
fn test_build_body_tool_result() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::tool_result("call_123", "Sunny, 25°C")],
);
let body = adapter.build_request_body(&req, false).unwrap();
let msg = &body["messages"][0];
assert_eq!(msg["role"], "user");
assert_eq!(msg["content"][0]["type"], "tool_result");
assert_eq!(msg["content"][0]["tool_use_id"], "call_123");
assert_eq!(msg["content"][0]["content"], "Sunny, 25°C");
assert_eq!(msg["content"][0]["is_error"], false);
}
#[test]
fn test_build_body_tool_result_error() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::tool_error("call_123", "API error")],
);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["messages"][0]["content"][0]["is_error"], true);
}
#[test]
fn test_build_body_image_base64() {
let adapter = AnthropicMessagesAdapter::new();
let parts = vec![
ContentPart::Text { text: "What's in this image? ".to_owned() },
ContentPart::ImageBase64 {
media_type: "image/png".to_owned(),
data: "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGA"
.to_owned(),
},
];
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::User { content: MessageContent::MultiPart(parts) }],
);
let body = adapter.build_request_body(&req, false).unwrap();
let content = &body["messages"][0]["content"];
assert_eq!(content[1]["type"], "image");
assert_eq!(content[1]["source"]["type"], "base64");
assert_eq!(content[1]["source"]["media_type"], "image/png");
}
#[test]
fn test_build_body_image_url() {
let adapter = AnthropicMessagesAdapter::new();
let parts = vec![ContentPart::ImageUrl {
url: "https://example.com/image.png".to_owned(),
detail: None,
}];
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::User { content: MessageContent::MultiPart(parts) }],
);
let body = adapter.build_request_body(&req, false).unwrap();
let content = &body["messages"][0]["content"];
assert_eq!(content[0]["type"], "image");
assert_eq!(content[0]["source"]["type"], "url");
assert_eq!(content[0]["source"]["url"], "https://example.com/image.png");
}
#[test]
fn test_build_body_tools_and_tool_choice() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hello")]);
req.tools = Some(vec![ToolDefinition {
name: "search".to_owned(),
description: "Search the web".to_owned(),
parameters: json!({"type": "object", "properties": {"q": {"type": "string"}}}),
strict: None,
}]);
req.tool_choice = Some(ToolChoice::Auto);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["tools"][0]["name"], "search");
assert_eq!(body["tools"][0]["description"], "Search the web");
assert_eq!(body["tool_choice"], "auto");
}
#[test]
fn test_build_body_tool_choice_specific() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.tool_choice = Some(ToolChoice::Specific { name: "search".to_owned() });
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["tool_choice"]["type"], "tool");
assert_eq!(body["tool_choice"]["name"], "search");
}
#[test]
fn test_build_body_tool_choice_disabled() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.tool_choice = Some(ToolChoice::Disabled);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["tool_choice"], "none");
}
#[test]
fn test_build_body_optional_params() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.temperature = Some(0.7);
req.top_p = Some(0.9);
req.top_k = Some(40);
req.stop = Some(vec!["\n".to_owned(), "END".to_owned()]);
req.max_tokens = Some(100);
let body = adapter.build_request_body(&req, false).unwrap();
assert!((body["temperature"].as_f64().unwrap() - 0.7).abs() < 1e-6);
assert!((body["top_p"].as_f64().unwrap() - 0.9).abs() < 1e-6);
assert_eq!(body["top_k"], 40);
assert_eq!(body["max_tokens"], 100);
assert_eq!(body["stop_sequences"][0], "\n");
assert_eq!(body["stop_sequences"][1], "END");
}
#[test]
fn test_build_body_reasoning_effort_low() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.reasoning_effort = Some(ReasoningEffort::Low);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["thinking"]["type"], "enabled");
assert_eq!(body["thinking"]["budget_tokens"], 1024);
}
#[test]
fn test_build_body_reasoning_effort_medium() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.reasoning_effort = Some(ReasoningEffort::Medium);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["thinking"]["budget_tokens"], 2048);
}
#[test]
fn test_build_body_reasoning_effort_high() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.reasoning_effort = Some(ReasoningEffort::High);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["thinking"]["budget_tokens"], 4096);
}
#[test]
fn test_build_body_no_model_uses_null() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest {
model: None,
messages: vec![Message::user("Hi")],
..CompletionRequest::default()
};
let body = adapter.build_request_body(&req, false).unwrap();
assert!(body["model"].is_null());
}
#[test]
fn test_build_body_stream_does_not_add_stream_flag() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body_stream = adapter.build_request_body(&req, true).unwrap();
let body_no_stream = adapter.build_request_body(&req, false).unwrap();
assert!(body_stream.get("stream").is_none());
assert_eq!(body_stream, body_no_stream);
}
#[test]
fn test_build_body_thinking_enabled() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.thinking = Some(crate::types::ThinkingConfig {
thinking_type: crate::types::ThinkingType::Enabled { budget_tokens: Some(8192) },
display: None,
});
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["thinking"]["type"], "enabled");
assert_eq!(body["thinking"]["budget_tokens"], 8192);
}
#[test]
fn test_build_body_thinking_adaptive() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.thinking = Some(crate::types::ThinkingConfig {
thinking_type: crate::types::ThinkingType::Adaptive,
display: None,
});
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["thinking"]["type"], "adaptive");
assert!(body["thinking"].get("budget_tokens").is_none());
}
#[test]
fn test_build_body_thinking_disabled_omitted() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.thinking = Some(crate::types::ThinkingConfig {
thinking_type: crate::types::ThinkingType::Disabled,
display: None,
});
let body = adapter.build_request_body(&req, false).unwrap();
assert!(body.get("thinking").is_none());
}
#[test]
fn test_build_body_thinking_precedence_over_reasoning_effort() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.reasoning_effort = Some(ReasoningEffort::High); req.thinking = Some(crate::types::ThinkingConfig {
thinking_type: crate::types::ThinkingType::Enabled { budget_tokens: Some(16000) },
display: None,
});
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["thinking"]["type"], "enabled");
assert_eq!(body["thinking"]["budget_tokens"], 16000);
}
#[test]
fn test_build_body_output_config_effort() {
let adapter =
AnthropicMessagesAdapter::new().with_effort_level(crate::types::EffortLevel::High);
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["output_config"]["effort"], "high");
}
#[test]
fn test_build_body_output_config_xhigh() {
let adapter =
AnthropicMessagesAdapter::new().with_effort_level(crate::types::EffortLevel::XHigh);
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["output_config"]["effort"], "xhigh");
}
#[test]
fn test_build_body_no_output_config_without_effort() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
assert!(body.get("output_config").is_none());
}
#[test]
fn test_build_body_metadata_user_id() {
let adapter = AnthropicMessagesAdapter::new();
let mut req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
req.user = Some("user-12345".to_owned());
let body = adapter.build_request_body(&req, false).unwrap();
assert_eq!(body["metadata"]["user_id"], "user-12345");
}
#[test]
fn test_build_body_no_metadata_without_user() {
let adapter = AnthropicMessagesAdapter::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
assert!(body.get("metadata").is_none());
}
#[test]
fn test_build_body_document_content_block() {
let adapter = AnthropicMessagesAdapter::new();
let parts = vec![
ContentPart::Text { text: "What's in this doc? ".to_owned() },
ContentPart::Document {
source: crate::types::DocumentSource::Base64 {
media_type: "application/pdf".to_owned(),
data: "base64doc".to_owned(),
},
title: Some("readme.pdf".to_owned()),
cache_control: None,
},
];
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::User { content: MessageContent::MultiPart(parts) }],
);
let body = adapter.build_request_body(&req, false).unwrap();
let content = &body["messages"][0]["content"];
assert_eq!(content[0]["type"], "text");
assert_eq!(content[1]["type"], "document");
assert_eq!(content[1]["source"]["type"], "base64");
assert_eq!(content[1]["source"]["media_type"], "application/pdf");
assert_eq!(content[1]["source"]["data"], "base64doc");
assert_eq!(content[1]["title"], "readme.pdf");
}
#[test]
fn test_build_body_document_url() {
let adapter = AnthropicMessagesAdapter::new();
let parts = vec![ContentPart::Document {
source: crate::types::DocumentSource::Url {
url: "https://example.com/doc.pdf".to_owned(),
},
title: None,
cache_control: Some(CacheControl::Ephemeral),
}];
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::User { content: MessageContent::MultiPart(parts) }],
);
let body = adapter.build_request_body(&req, false).unwrap();
let content = &body["messages"][0]["content"];
assert_eq!(content[0]["type"], "document");
assert_eq!(content[0]["source"]["type"], "url");
assert_eq!(content[0]["source"]["url"], "https://example.com/doc.pdf");
assert_eq!(content[0]["cache_control"]["type"], "ephemeral");
}
#[test]
fn test_build_body_tool_reference_block() {
let adapter = AnthropicMessagesAdapter::new();
let parts = vec![ContentPart::ToolReference {
tool_name: "search".to_owned(),
cache_control: Some(CacheControl::Ephemeral),
}];
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::User { content: MessageContent::MultiPart(parts) }],
);
let body = adapter.build_request_body(&req, false).unwrap();
let content = &body["messages"][0]["content"];
assert_eq!(content[0]["type"], "tool_reference");
assert_eq!(content[0]["tool_name"], "search");
assert_eq!(content[0]["cache_control"]["type"], "ephemeral");
}
#[test]
fn test_build_body_redacted_thinking_block() {
let adapter = AnthropicMessagesAdapter::new();
let parts =
vec![ContentPart::RedactedThinking { data: "encrypted_thinking_data".to_owned() }];
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::User { content: MessageContent::MultiPart(parts) }],
);
let body = adapter.build_request_body(&req, false).unwrap();
let content = &body["messages"][0]["content"];
assert_eq!(content[0]["type"], "redacted_thinking");
assert_eq!(content[0]["data"], "encrypted_thinking_data");
}
#[test]
fn test_auth_headers_api_key() {
let adapter = AnthropicMessagesAdapter::new();
let auth = AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-ant-abc123".to_owned(),
};
let headers = adapter.build_auth_headers(&auth);
assert_eq!(headers.len(), 2); assert_eq!(headers[0].0, "x-api-key");
assert_eq!(headers[0].1, "sk-ant-abc123");
assert_eq!(headers[1].0, "anthropic-version");
assert_eq!(headers[1].1, "2023-06-01");
}
#[test]
fn test_auth_headers_bearer_returns_version_only() {
let adapter = AnthropicMessagesAdapter::new();
let auth = AuthMethod::Bearer { token: "sk-xxx".to_owned() };
let headers = adapter.build_auth_headers(&auth);
assert_eq!(headers.len(), 1); assert_eq!(headers[0].0, "anthropic-version");
assert_eq!(headers[0].1, "2023-06-01");
}
#[test]
fn test_auth_headers_none_returns_version_only() {
let adapter = AnthropicMessagesAdapter::new();
let headers = adapter.build_auth_headers(&AuthMethod::None);
assert_eq!(headers.len(), 1); assert_eq!(headers[0].0, "anthropic-version");
assert_eq!(headers[0].1, "2023-06-01");
}
#[test]
fn test_auth_headers_with_beta_headers() {
let adapter = AnthropicMessagesAdapter::new().with_beta_headers(vec![
"prompt-caching-2025-02-19".to_owned(),
"tools-2025-04-01".to_owned(),
]);
let auth = AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-ant-abc123".to_owned(),
};
let headers = adapter.build_auth_headers(&auth);
assert_eq!(headers.len(), 4); assert_eq!(headers[0].0, "x-api-key");
assert_eq!(headers[1].0, "anthropic-version");
assert_eq!(headers[2].0, "anthropic-beta");
assert_eq!(headers[2].1, "prompt-caching-2025-02-19");
assert_eq!(headers[3].0, "anthropic-beta");
assert_eq!(headers[3].1, "tools-2025-04-01");
}
#[test]
fn test_parse_response_text_only() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "Hello! How can I help you?"}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.content.as_deref(), Some("Hello! How can I help you?"));
assert!(resp.thinking.is_none());
assert!(resp.tool_calls.is_empty());
assert_eq!(resp.model, "claude-3-5-sonnet-20241022");
assert_eq!(resp.finish_reason, FinishReason::Stop);
assert_eq!(resp.usage.prompt_tokens, 10);
assert_eq!(resp.usage.completion_tokens, 20);
assert_eq!(resp.usage.total_tokens, 30);
}
#[test]
fn test_parse_response_with_tool_use() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "Let me search for that."},
{"type": "tool_use", "id": "toolu_abc", "name": "search", "input": {"q": "weather"}}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "tool_use",
"usage": {
"input_tokens": 15,
"output_tokens": 30
}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.content.as_deref(), Some("Let me search for that."));
assert_eq!(resp.tool_calls.len(), 1);
assert_eq!(resp.tool_calls[0].id, "toolu_abc");
assert_eq!(resp.tool_calls[0].function_name, "search");
assert_eq!(resp.tool_calls[0].arguments["q"], "weather");
assert_eq!(resp.finish_reason, FinishReason::ToolCall);
}
#[test]
fn test_parse_response_with_cache_info() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "Hello"}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 100,
"output_tokens": 5,
"cache_creation_input_tokens": 0,
"cache_read_input_tokens": 50
}
});
let resp = adapter.parse_response(&body).unwrap();
assert!(resp.cache_info.is_some());
assert_eq!(resp.cache_info.as_ref().unwrap().cached_tokens, 50);
assert_eq!(resp.usage.cached_tokens, Some(50));
}
#[test]
fn test_parse_response_with_thinking() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "I need to reason step by step..."},
{"type": "text", "text": "Here is the answer."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.thinking.as_deref(), Some("I need to reason step by step..."));
assert_eq!(resp.content.as_deref(), Some("Here is the answer."));
}
#[test]
fn test_parse_response_empty_content() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 5, "output_tokens": 0}
});
let resp = adapter.parse_response(&body).unwrap();
assert!(resp.content.is_none());
assert!(resp.tool_calls.is_empty());
}
#[test]
fn test_parse_response_stop_sequence() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Hello"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "stop_sequence",
"usage": {"input_tokens": 5, "output_tokens": 5}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.finish_reason, FinishReason::Stop);
}
#[test]
fn test_parse_response_max_tokens() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Partial"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "max_tokens",
"usage": {"input_tokens": 5, "output_tokens": 100}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.finish_reason, FinishReason::MaxTokens);
}
#[test]
fn test_parse_response_unknown_stop_reason_maps_to_stop() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Hmm"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "content_blocked",
"usage": {"input_tokens": 5, "output_tokens": 3}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.finish_reason, FinishReason::Stop);
}
#[test]
fn test_parse_response_pause_turn() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Let me pause here."}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "pause_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.finish_reason, FinishReason::PauseTurn);
}
#[test]
fn test_parse_response_refusal() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "I cannot answer that."}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "refusal",
"usage": {"input_tokens": 10, "output_tokens": 5}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.finish_reason, FinishReason::Refusal);
}
#[test]
fn test_parse_response_thinking_with_signature() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Step by step reasoning...", "signature": "Eq4BCkkwIQY..."},
{"type": "text", "text": "The answer is 42."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 20}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.thinking.as_deref(), Some("Step by step reasoning..."));
assert_eq!(resp.signature.as_deref(), Some("Eq4BCkkwIQY..."));
}
#[test]
fn test_parse_response_redacted_thinking() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "redacted_thinking", "data": "encrypted_thinking_data_here"},
{"type": "text", "text": "Answer based on redacted context."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 15}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.redacted_thinking.as_deref(), Some("encrypted_thinking_data_here"));
}
#[test]
fn test_parse_response_citations_in_text() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "According to the document, the answer is 42.",
"citations": [
{
"type": "char_location",
"cited_text": "the answer is 42",
"start_index": 28,
"end_index": 47,
"document_index": 0
}
]
}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 20}
});
let resp = adapter.parse_response(&body).unwrap();
assert!(resp.citations.is_some());
let citations = resp.citations.unwrap();
assert_eq!(citations.len(), 1);
match &citations[0] {
Citation::CharLocation { cited_text, start_index, end_index, document_index } => {
assert_eq!(cited_text, "the answer is 42");
assert_eq!(*start_index, 28);
assert_eq!(*end_index, 47);
assert_eq!(*document_index, 0);
}
_ => panic!("Expected CharLocation citation"),
}
}
#[test]
fn test_parse_response_cache_creation() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Hello"}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 100,
"output_tokens": 20,
"cache_creation": {
"ephemeral_5m_input_tokens": 80,
"ephemeral_1h_input_tokens": 50
}
}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.usage.cache_write_5m_input_tokens, Some(80));
assert_eq!(resp.usage.cache_write_1h_input_tokens, Some(50));
}
#[test]
fn test_parse_response_document_block_skipped() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": "base64..."}},
{"type": "text", "text": "Document referenced above."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 20, "output_tokens": 5}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.content.as_deref(), Some("Document referenced above."));
}
#[test]
fn test_parse_response_unknown_block_skipped() {
let adapter = AnthropicMessagesAdapter::new();
let body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "unknown_block", "data": "some data"},
{"type": "text", "text": "After unknown block."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 5}
});
let resp = adapter.parse_response(&body).unwrap();
assert_eq!(resp.content.as_deref(), Some("After unknown block."));
}
#[test]
fn test_sse_message_start_ignored() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_start","message":{"id":"msg_1","type":"message","role":"assistant","content":[],"model":"claude-3-5-sonnet","stop_reason":null,"usage":{"input_tokens":5,"output_tokens":0}}}"#;
let result = adapter.parse_sse_event(data).unwrap();
assert!(result.is_none());
}
#[test]
fn test_sse_content_block_start_tool_use() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_abc","name":"search","input":{}}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::ToolCallDelta { index, id, function_name, arguments_delta } => {
assert_eq!(index, 0);
assert_eq!(id.as_deref(), Some("toolu_abc"));
assert_eq!(function_name.as_deref(), Some("search"));
assert_eq!(arguments_delta, "");
}
other => panic!("Expected ToolCallDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_start_thinking() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"Let me think..."}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::ThinkingDelta { delta } => {
assert_eq!(delta, "Let me think...");
}
other => panic!("Expected ThinkingDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_start_text_ignored() {
let adapter = AnthropicMessagesAdapter::new();
let data =
r#"{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}"#;
let result = adapter.parse_sse_event(data).unwrap();
assert!(result.is_none());
}
#[test]
fn test_sse_content_block_delta_text() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::ContentDelta { delta } => {
assert_eq!(delta, "Hello");
}
other => panic!("Expected ContentDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_delta_input_json() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"city\": \"Tokyo\"}"}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::ToolCallDelta { index, id, function_name, arguments_delta } => {
assert_eq!(index, 0);
assert!(id.is_none());
assert!(function_name.is_none());
assert_eq!(arguments_delta, "{\"city\": \"Tokyo\"}");
}
other => panic!("Expected ToolCallDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_delta_thinking() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"thinking_delta","thinking":" deeper"}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::ThinkingDelta { delta } => {
assert_eq!(delta, " deeper");
}
other => panic!("Expected ThinkingDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_delta_signature() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"Eq4BCkkwIQYizEOE"}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::SignatureDelta { signature } => {
assert_eq!(signature, "Eq4BCkkwIQYizEOE");
}
other => panic!("Expected SignatureDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_delta_citations() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"citations_delta","citations":[{"type":"char_location","cited_text":"answer is 42","start_index":0,"end_index":12,"document_index":0}]}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::CitationsDelta { citations } => {
assert_eq!(citations[0]["cited_text"], "answer is 42");
assert_eq!(citations[0]["document_index"], 0);
}
other => panic!("Expected CitationsDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_start_redacted_thinking() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_start","index":0,"content_block":{"type":"redacted_thinking","data":"encrypted_thinking_blob_abc123"}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::RedactedThinkingDelta { data } => {
assert_eq!(data, "encrypted_thinking_blob_abc123");
}
other => panic!("Expected RedactedThinkingDelta, got {:?}", other),
}
}
#[test]
fn test_sse_content_block_start_redacted_thinking_empty_data() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_start","index":0,"content_block":{"type":"redacted_thinking","data":""}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::RedactedThinkingDelta { data } => {
assert!(data.is_empty());
}
other => panic!("Expected RedactedThinkingDelta, got {:?}", other),
}
}
#[test]
fn test_sse_signature_delta_without_thinking() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"standalone_sig"}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::SignatureDelta { signature } => {
assert_eq!(signature, "standalone_sig");
}
other => panic!("Expected SignatureDelta, got {:?}", other),
}
}
#[test]
fn test_sse_citations_delta_empty_array() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_delta","index":0,"delta":{"type":"citations_delta","citations":[]}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::CitationsDelta { citations } => {
assert!(citations.as_array().unwrap().is_empty());
}
other => panic!("Expected CitationsDelta, got {:?}", other),
}
}
#[test]
fn test_sse_message_delta_pause_turn() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"pause_turn"},"usage":{"input_tokens":10,"output_tokens":5}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { finish_reason, .. } => {
assert_eq!(finish_reason, FinishReason::PauseTurn);
}
other => panic!("Expected Done(PauseTurn), got {:?}", other),
}
}
#[test]
fn test_sse_message_delta_refusal() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"refusal"},"usage":{"input_tokens":10,"output_tokens":3}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { finish_reason, .. } => {
assert_eq!(finish_reason, FinishReason::Refusal);
}
other => panic!("Expected Done(Refusal), got {:?}", other),
}
}
#[test]
fn test_sse_message_delta_with_done() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":10,"output_tokens":20}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { finish_reason, usage } => {
assert_eq!(finish_reason, FinishReason::Stop);
let u = usage.unwrap();
assert_eq!(u.prompt_tokens, 10);
assert_eq!(u.completion_tokens, 20);
assert_eq!(u.total_tokens, 30);
}
other => panic!("Expected Done, got {:?}", other),
}
}
#[test]
fn test_sse_message_delta_with_tool_use() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":10,"output_tokens":25}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { finish_reason, usage } => {
assert_eq!(finish_reason, FinishReason::ToolCall);
assert!(usage.is_some());
}
other => panic!("Expected Done, got {:?}", other),
}
}
#[test]
fn test_sse_message_delta_with_cache_usage() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"input_tokens":100,"output_tokens":5,"cache_read_input_tokens":50}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { usage, .. } => {
let u = usage.unwrap();
assert_eq!(u.cached_tokens, Some(50));
}
other => panic!("Expected Done, got {:?}", other),
}
}
#[test]
fn test_sse_message_stop_ignored() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_stop"}"#;
let result = adapter.parse_sse_event(data).unwrap();
assert!(result.is_none());
}
#[test]
fn test_sse_content_block_stop_ignored() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"content_block_stop","index":0}"#;
let result = adapter.parse_sse_event(data).unwrap();
assert!(result.is_none());
}
#[test]
fn test_sse_ping_ignored() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"ping"}"#;
let result = adapter.parse_sse_event(data).unwrap();
assert!(result.is_none());
}
#[test]
fn test_sse_unknown_event_ignored() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"unknown_event"}"#;
let result = adapter.parse_sse_event(data).unwrap();
assert!(result.is_none());
}
#[test]
fn test_sse_missing_type_returns_error() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"foo":"bar"}"#;
let result = adapter.parse_sse_event(data);
assert!(result.is_err());
}
#[test]
fn test_sse_invalid_json_returns_error() {
let adapter = AnthropicMessagesAdapter::new();
let result = adapter.parse_sse_event("not json");
assert!(result.is_err());
}
#[test]
fn test_sse_message_delta_max_tokens() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"max_tokens"},"usage":{"input_tokens":5,"output_tokens":100}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { finish_reason, .. } => {
assert_eq!(finish_reason, FinishReason::MaxTokens);
}
other => panic!("Expected Done, got {:?}", other),
}
}
#[test]
fn test_sse_message_delta_unknown_reason_maps_to_stop() {
let adapter = AnthropicMessagesAdapter::new();
let data = r#"{"type":"message_delta","delta":{"stop_reason":"content_blocked"},"usage":{"input_tokens":5,"output_tokens":3}}"#;
let result = adapter.parse_sse_event(data).unwrap();
match result.unwrap() {
StreamEvent::Done { finish_reason, .. } => {
assert_eq!(finish_reason, FinishReason::Stop);
}
other => panic!("Expected Done, got {:?}", other),
}
}
#[test]
fn test_handle_error_529() {
let err = handle_error_response(529, &HeaderMap::new(), "Overloaded");
assert!(matches!(err, ProviderError::Overloaded));
}
#[test]
fn test_default_impl() {
let adapter = AnthropicMessagesAdapter::new();
assert_eq!(adapter.protocol_name(), "anthropic");
}
#[tokio::test]
async fn test_non_streaming_completion() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_123",
"type": "message",
"role": "assistant",
"content": [
{"type": "text", "text": "Hello! I'm Claude."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 10,
"output_tokens": 20
}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(completion.content.as_deref(), Some("Hello! I'm Claude."));
assert_eq!(completion.finish_reason, FinishReason::Stop);
assert_eq!(completion.usage.prompt_tokens, 10);
assert_eq!(completion.usage.completion_tokens, 20);
}
#[tokio::test]
async fn test_streaming_sse_events() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let sse_body = "\
event: message_start\n\
data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3-5-sonnet\",\"usage\":{\"input_tokens\":5,\"output_tokens\":0}}}\n\
\n\
event: content_block_start\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Hello\"}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\" world\"}}\n\
\n\
event: content_block_stop\n\
data: {\"type\":\"content_block_stop\",\"index\":0}\n\
\n\
event: message_delta\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":5,\"output_tokens\":3}}\n\
\n\
event: message_stop\n\
data: {\"type\":\"message_stop\"}\n\
\n";
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(sse_body.to_owned())
.insert_header("content-type", "text/event-stream"),
)
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/messages", mock_server.uri()))
.header("x-api-key", "sk-test")
.json(&json!({"model": "claude-3-5-sonnet", "max_tokens": 100, "messages": [{"role": "user", "content": [{"type": "text", "text": "Hi"}]}]}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
let stream = create_sse_stream(resp, None);
let lines: Vec<String> = stream
.filter_map(|r: Result<String, ProviderError>| futures::future::ready(r.ok()))
.collect()
.await;
let mut events: Vec<StreamEvent> = Vec::new();
for line in &lines {
if let Some(data) = line.strip_prefix("data: ") {
if let Ok(Some(event)) = adapter.parse_sse_event(data) {
events.push(event);
}
}
}
assert_eq!(events.len(), 3, "Expected 3 events, got {:?}", events);
assert!(matches!(
events[0],
StreamEvent::ContentDelta { ref delta } if delta == "Hello"
));
assert!(matches!(
events[1],
StreamEvent::ContentDelta { ref delta } if delta == " world"
));
assert!(matches!(
events[2],
StreamEvent::Done { finish_reason: FinishReason::Stop, usage: Some(_) }
));
}
#[tokio::test]
async fn test_streaming_tool_use_events() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let sse_body = "\
event: message_start\n\
data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"content\":[],\"model\":\"claude-3-5-sonnet\",\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}\n\
\n\
event: content_block_start\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_abc\",\"name\":\"search\",\"input\":{}}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"q\\\":\"}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\" \\\"weather\\\"}\"}}\n\
\n\
event: content_block_stop\n\
data: {\"type\":\"content_block_stop\",\"index\":0}\n\
\n\
event: message_delta\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\"},\"usage\":{\"input_tokens\":10,\"output_tokens\":15}}\n\
\n\
event: message_stop\n\
data: {\"type\":\"message_stop\"}\n\
\n";
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(sse_body.to_owned())
.insert_header("content-type", "text/event-stream"),
)
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/messages", mock_server.uri()))
.header("x-api-key", "sk-test")
.json(&json!({"model": "claude-3-5-sonnet", "max_tokens": 100, "messages": []}))
.send()
.await
.unwrap();
let stream = create_sse_stream(resp, None);
let lines: Vec<String> = stream
.filter_map(|r: Result<String, ProviderError>| futures::future::ready(r.ok()))
.collect()
.await;
let mut events: Vec<StreamEvent> = Vec::new();
for line in &lines {
if let Some(data) = line.strip_prefix("data: ") {
if let Ok(Some(event)) = adapter.parse_sse_event(data) {
events.push(event);
}
}
}
assert_eq!(events.len(), 4, "Expected 4 events, got {:?}", events);
assert!(
matches!(&events[0], StreamEvent::ToolCallDelta { id: Some(id), function_name: Some(fname), .. }
if id == "toolu_abc" && fname == "search"
)
);
assert!(matches!(&events[1], StreamEvent::ToolCallDelta { arguments_delta, .. }
if arguments_delta == "{\"q\":"
));
assert!(matches!(
&events[3],
StreamEvent::Done { finish_reason: FinishReason::ToolCall, .. }
));
}
#[tokio::test]
async fn test_integration_error_401() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let adapter = AnthropicMessagesAdapter::new();
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-invalid".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 401);
let empty_headers = reqwest::header::HeaderMap::new();
let err = handle_error_response(401, &empty_headers, "Unauthorized");
assert!(matches!(err, ProviderError::Auth(_)));
}
#[tokio::test]
async fn test_integration_error_429() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(429).set_body_string("Rate limited"))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let adapter = AnthropicMessagesAdapter::new();
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 429);
}
#[tokio::test]
async fn test_integration_error_529() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(529).set_body_string("Overloaded"))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let adapter = AnthropicMessagesAdapter::new();
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert_eq!(resp.status().as_u16(), 529);
}
#[tokio::test]
async fn test_integration_timeout() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(5)))
.mount(&mock_server)
.await;
let client =
reqwest::Client::builder().timeout(Duration::from_millis(100)).build().unwrap();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let adapter = AnthropicMessagesAdapter::new();
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let result = build_request(
&client,
&format!("{}/messages", mock_server.uri()),
body,
headers,
Some(Duration::from_millis(50)),
)
.await;
assert!(result.is_err());
match result.unwrap_err() {
ProviderError::Timeout { .. } => {} other => panic!("Expected Timeout error, got {:?}", other),
}
}
#[tokio::test]
async fn test_wiremock_thinking_with_signature() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_thinking_sig",
"type": "message",
"role": "assistant",
"content": [
{"type": "thinking", "thinking": "Let me reason step by step...", "signature": "Eq4BCkkwIQYizEOE"},
{"type": "text", "text": "The answer is 42."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 10, "output_tokens": 20}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Think deeply")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(completion.thinking.as_deref(), Some("Let me reason step by step..."));
assert_eq!(completion.signature.as_deref(), Some("Eq4BCkkwIQYizEOE"));
assert_eq!(completion.content.as_deref(), Some("The answer is 42."));
}
#[tokio::test]
async fn test_wiremock_redacted_thinking_block() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_redacted",
"type": "message",
"role": "assistant",
"content": [
{"type": "redacted_thinking", "data": "encrypted_data_abc123"},
{"type": "text", "text": "I processed the redacted thinking."}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 15, "output_tokens": 10}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(completion.redacted_thinking.as_deref(), Some("encrypted_data_abc123"));
assert_eq!(completion.content.as_deref(), Some("I processed the redacted thinking."));
}
#[tokio::test]
async fn test_wiremock_citations_in_text() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_citations",
"type": "message",
"role": "assistant",
"content": [
{
"type": "text",
"text": "According to the report, revenue grew by 20%.",
"citations": [
{
"type": "char_location",
"cited_text": "revenue grew by 20%",
"start_index": 24,
"end_index": 43,
"document_index": 0
},
{
"type": "page_location",
"cited_text": "the report",
"start_page_number": 1,
"end_page_number": 1,
"document_index": 0
}
]
}
],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {"input_tokens": 50, "output_tokens": 25}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req =
CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Analyze the report")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(
completion.content.as_deref(),
Some("According to the report, revenue grew by 20%.")
);
let citations = completion.citations.expect("citations should be present");
assert_eq!(citations.len(), 2);
assert!(matches!(citations[0], Citation::CharLocation { .. }));
assert!(matches!(citations[1], Citation::PageLocation { .. }));
}
#[tokio::test]
async fn test_wiremock_pause_turn_stop_reason() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_pause",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Let me pause here for your input."}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "pause_turn",
"usage": {"input_tokens": 10, "output_tokens": 8}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Hi")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(completion.finish_reason, FinishReason::PauseTurn);
}
#[tokio::test]
async fn test_wiremock_refusal_stop_reason() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_refusal",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "I cannot assist with that request."}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "refusal",
"usage": {"input_tokens": 10, "output_tokens": 7}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req = CompletionRequest::new(
"claude-3-5-sonnet",
vec![Message::user("Do something harmful")],
);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(completion.finish_reason, FinishReason::Refusal);
}
#[tokio::test]
async fn test_streaming_full_anthropic_sequence() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let sse_body = "\
event: message_start\n\
data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_full\",\"type\":\"message\",\"role\":\"assistant\",\"content\":[],\"model\":\"claude-3-5-sonnet\",\"usage\":{\"input_tokens\":10,\"output_tokens\":0}}}\n\
\n\
event: content_block_start\n\
data: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"Here is the answer.\"}}\n\
\n\
event: content_block_stop\n\
data: {\"type\":\"content_block_stop\",\"index\":0}\n\
\n\
event: content_block_start\n\
data: {\"type\":\"content_block_start\",\"index\":1,\"content_block\":{\"type\":\"thinking\",\"thinking\":\"Let me reason step by step...\"}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\" deeper analysis\"}}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":1,\"delta\":{\"type\":\"signature_delta\",\"signature\":\"Eq4BCkkwIQYizEOE\"}}\n\
\n\
event: content_block_stop\n\
data: {\"type\":\"content_block_stop\",\"index\":1}\n\
\n\
event: content_block_start\n\
data: {\"type\":\"content_block_start\",\"index\":2,\"content_block\":{\"type\":\"redacted_thinking\",\"data\":\"encrypted_blob_xyz789\"}}\n\
\n\
event: content_block_stop\n\
data: {\"type\":\"content_block_stop\",\"index\":2}\n\
\n\
event: content_block_delta\n\
data: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"citations_delta\",\"citations\":[{\"type\":\"char_location\",\"cited_text\":\"the answer\",\"start_index\":8,\"end_index\":19,\"document_index\":0}]}}\n\
\n\
event: message_delta\n\
data: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\"},\"usage\":{\"input_tokens\":10,\"output_tokens\":25}}\n\
\n\
event: message_stop\n\
data: {\"type\":\"message_stop\"}\n\
\n";
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(
ResponseTemplate::new(200)
.set_body_string(sse_body.to_owned())
.insert_header("content-type", "text/event-stream"),
)
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let resp = client
.post(format!("{}/messages", mock_server.uri()))
.header("x-api-key", "sk-test")
.json(&json!({"model": "claude-3-5-sonnet", "max_tokens": 100, "messages": [{"role": "user", "content": [{"type": "text", "text": "Think deeply"}]}]}))
.send()
.await
.unwrap();
assert!(resp.status().is_success());
let stream = create_sse_stream(resp, None);
let lines: Vec<String> = stream
.filter_map(|r: Result<String, ProviderError>| futures::future::ready(r.ok()))
.collect()
.await;
let mut events: Vec<StreamEvent> = Vec::new();
for line in &lines {
if let Some(data) = line.strip_prefix("data: ") {
if let Ok(Some(event)) = adapter.parse_sse_event(data) {
events.push(event);
}
}
}
assert_eq!(events.len(), 7, "Expected 7 events, got {:?}", events);
assert!(matches!(
&events[0],
StreamEvent::ContentDelta { delta } if delta == "Here is the answer."
));
assert!(matches!(
&events[1],
StreamEvent::ThinkingDelta { delta } if delta == "Let me reason step by step..."
));
assert!(matches!(
&events[2],
StreamEvent::ThinkingDelta { delta } if delta == " deeper analysis"
));
assert!(matches!(
&events[3],
StreamEvent::SignatureDelta { signature } if signature == "Eq4BCkkwIQYizEOE"
));
assert!(matches!(
&events[4],
StreamEvent::RedactedThinkingDelta { data } if data == "encrypted_blob_xyz789"
));
assert!(matches!(&events[5], StreamEvent::CitationsDelta { .. }));
assert!(matches!(
&events[6],
StreamEvent::Done { finish_reason: FinishReason::Stop, usage: Some(_) }
));
if let StreamEvent::CitationsDelta { citations } = &events[5] {
assert_eq!(citations[0]["cited_text"], "the answer");
}
}
#[tokio::test]
async fn test_wiremock_cache_creation_detail() {
let adapter = AnthropicMessagesAdapter::new();
let mock_server = MockServer::start().await;
let response_body = json!({
"id": "msg_cache",
"type": "message",
"role": "assistant",
"content": [{"type": "text", "text": "Cached response."}],
"model": "claude-3-5-sonnet-20241022",
"stop_reason": "end_turn",
"usage": {
"input_tokens": 200,
"output_tokens": 15,
"cache_creation": {
"ephemeral_5m_input_tokens": 150,
"ephemeral_1h_input_tokens": 100
}
}
});
Mock::given(method("POST"))
.and(path("/messages"))
.respond_with(ResponseTemplate::new(200).set_body_json(response_body))
.mount(&mock_server)
.await;
let client = reqwest::Client::new();
let req =
CompletionRequest::new("claude-3-5-sonnet", vec![Message::user("Long document...")]);
let body = adapter.build_request_body(&req, false).unwrap();
let headers = adapter.build_auth_headers(&AuthMethod::ApiKey {
header_name: "x-api-key".to_owned(),
key: "sk-test".to_owned(),
});
let resp =
build_request(&client, &format!("{}/messages", mock_server.uri()), body, headers, None)
.await
.unwrap();
assert!(resp.status().is_success());
let response_json: Value = resp.json().await.unwrap();
let completion = adapter.parse_response(&response_json).unwrap();
assert_eq!(completion.usage.prompt_tokens, 200);
assert_eq!(completion.usage.completion_tokens, 15);
assert_eq!(completion.usage.cache_write_5m_input_tokens, Some(150));
assert_eq!(completion.usage.cache_write_1h_input_tokens, Some(100));
}