use siumai::prelude::*;
use siumai::stream::{ProcessedEvent, StreamProcessor};
use siumai::types::{ChatRequest, ContentPart, ProviderParams};
use siumai::user_builder;
use std::time::Duration;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_creation_with_macros() {
let user_msg = user!("Hello, world!");
assert_eq!(user_msg.role, MessageRole::User);
let system_msg = system!("You are a helpful assistant");
assert_eq!(system_msg.role, MessageRole::System);
let assistant_msg = assistant!("I'm here to help");
assert_eq!(assistant_msg.role, MessageRole::Assistant);
match user_msg.content {
MessageContent::Text(text) => assert_eq!(text, "Hello, world!"),
_ => panic!("Expected text content"),
}
}
#[test]
fn test_multimodal_message_creation() {
let multimodal_msg = user_builder!("Analyze this image")
.with_image(
"https://example.com/image.jpg".to_string(),
Some("high".to_string()),
)
.build();
match multimodal_msg.content {
MessageContent::MultiModal(parts) => {
assert_eq!(parts.len(), 2);
if let ContentPart::Text { text } = &parts[0] {
assert_eq!(text, "Analyze this image");
} else {
panic!("Expected text part");
}
if let ContentPart::Image { image_url, detail } = &parts[1] {
assert_eq!(image_url, "https://example.com/image.jpg");
assert_eq!(detail.as_ref().unwrap(), "high");
} else {
panic!("Expected image part");
}
}
_ => panic!("Expected multimodal content"),
}
}
#[test]
fn test_chat_request_builder() {
let messages = vec![
system!("You are a helpful assistant"),
user!("What is the capital of France?"),
];
let request = ChatRequest::builder()
.messages(messages)
.common_params(CommonParams {
model: "gpt-4".to_string(),
temperature: Some(0.7),
max_tokens: Some(1000),
top_p: Some(0.9),
stop_sequences: None,
seed: Some(42),
})
.build();
assert_eq!(request.messages.len(), 2);
assert_eq!(request.common_params.model, "gpt-4");
assert_eq!(request.common_params.temperature, Some(0.7));
assert_eq!(request.common_params.max_tokens, Some(1000));
}
#[test]
fn test_provider_params() {
let provider_params = ProviderParams::new()
.with_param("temperature", 0.8)
.with_param("max_tokens", 2000)
.with_param("custom_setting", "value");
let temp: Option<f64> = provider_params.get("temperature");
assert_eq!(temp, Some(0.8));
let tokens: Option<u32> = provider_params.get("max_tokens");
assert_eq!(tokens, Some(2000));
let custom: Option<String> = provider_params.get("custom_setting");
assert_eq!(custom, Some("value".to_string()));
let missing: Option<String> = provider_params.get("non_existent");
assert_eq!(missing, None);
}
#[test]
fn test_provider_capabilities() {
let capabilities = ProviderCapabilities::new()
.with_chat()
.with_streaming()
.with_tools()
.with_vision()
.with_custom_feature("structured_output", true);
assert!(capabilities.supports("chat"));
assert!(capabilities.supports("streaming"));
assert!(capabilities.supports("tools"));
assert!(capabilities.supports("vision"));
assert!(capabilities.supports("structured_output"));
assert!(!capabilities.supports("audio"));
assert!(!capabilities.supports("non_existent"));
}
#[test]
fn test_error_handling() {
let api_error = LlmError::api_error(404, "Not found");
assert_eq!(api_error.status_code(), Some(404));
assert!(!api_error.is_retryable());
assert!(!api_error.is_auth_error());
let auth_error = LlmError::AuthenticationError("Invalid API key".to_string());
assert!(auth_error.is_auth_error());
assert!(!auth_error.is_retryable());
let rate_limit_error = LlmError::RateLimitError("Too many requests".to_string());
assert!(rate_limit_error.is_retryable());
assert!(rate_limit_error.is_rate_limit_error());
let server_error = LlmError::api_error(500, "Internal server error");
assert!(server_error.is_retryable());
assert_eq!(server_error.status_code(), Some(500));
}
#[test]
fn test_http_config() {
let mut http_config = HttpConfig {
timeout: Some(Duration::from_secs(60)),
..Default::default()
};
http_config
.headers
.insert("Custom-Header".to_string(), "value".to_string());
assert_eq!(http_config.timeout, Some(Duration::from_secs(60)));
assert_eq!(
http_config.headers.get("Custom-Header"),
Some(&"value".to_string())
);
assert!(http_config.user_agent.is_some());
}
#[test]
fn test_usage_statistics() {
let mut usage1 = Usage {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
reasoning_tokens: None,
cached_tokens: None,
};
let usage2 = Usage {
prompt_tokens: 200,
completion_tokens: 75,
total_tokens: 275,
reasoning_tokens: Some(25),
cached_tokens: Some(10),
};
usage1.merge(&usage2);
assert_eq!(usage1.prompt_tokens, 300);
assert_eq!(usage1.completion_tokens, 125);
assert_eq!(usage1.total_tokens, 425);
assert_eq!(usage1.reasoning_tokens, Some(25));
}
#[test]
fn test_stream_processor() {
let mut processor = StreamProcessor::new();
let content_event = ChatStreamEvent::ContentDelta {
delta: "Hello".to_string(),
index: None,
};
let processed = processor.process_event(content_event);
match processed {
ProcessedEvent::ContentUpdate {
delta, accumulated, ..
} => {
assert_eq!(delta, "Hello");
assert_eq!(accumulated, "Hello");
}
_ => panic!("Expected ContentUpdate"),
}
let content_event2 = ChatStreamEvent::ContentDelta {
delta: " World".to_string(),
index: None,
};
let processed2 = processor.process_event(content_event2);
match processed2 {
ProcessedEvent::ContentUpdate {
delta, accumulated, ..
} => {
assert_eq!(delta, " World");
assert_eq!(accumulated, "Hello World");
}
_ => panic!("Expected ContentUpdate"),
}
}
#[test]
fn test_enhanced_parameter_validation() {
use siumai::params::EnhancedParameterValidator;
let params = CommonParams {
model: "gpt-4".to_string(),
temperature: Some(0.7),
max_tokens: Some(1000),
top_p: Some(0.9),
stop_sequences: None,
seed: Some(42),
};
let result =
EnhancedParameterValidator::validate_for_provider(¶ms, &ProviderType::OpenAi);
assert!(result.is_ok());
let report = result.unwrap();
assert!(!report.has_errors());
assert!(!report.valid_params.is_empty());
}
#[test]
fn test_retry_policy() {
use siumai::retry::RetryPolicy;
use std::time::Duration;
let policy = RetryPolicy::new()
.with_max_attempts(5)
.with_initial_delay(Duration::from_millis(100))
.with_backoff_multiplier(2.0)
.with_jitter(false);
assert_eq!(policy.max_attempts, 5);
assert_eq!(policy.initial_delay, Duration::from_millis(100));
assert_eq!(policy.backoff_multiplier, 2.0);
assert_eq!(policy.calculate_delay(0), Duration::from_millis(100));
assert_eq!(policy.calculate_delay(1), Duration::from_millis(200));
assert_eq!(policy.calculate_delay(2), Duration::from_millis(400));
}
#[test]
fn test_error_classification() {
use siumai::error::ErrorCategory;
let error = LlmError::ApiError {
code: 429,
message: "Rate limit exceeded".to_string(),
details: None,
};
assert_eq!(error.category(), ErrorCategory::RateLimit);
assert!(error.is_retryable());
assert_eq!(error.max_retry_attempts(), 3);
}
}
#[cfg(test)]
mod builder_tests {
use super::*;
#[test]
fn test_llm_builder_creation() {
let builder = LlmBuilder::new();
let _openai_builder = builder
.openai()
.model("gpt-4")
.temperature(0.7)
.max_tokens(1000);
}
#[test]
fn test_builder_with_http_client() {
let custom_client = reqwest::Client::builder()
.timeout(Duration::from_secs(60))
.build()
.unwrap();
let builder = LlmBuilder::new()
.with_http_client(custom_client)
.with_timeout(Duration::from_secs(30));
let _openai_builder = builder.openai().model("gpt-4").temperature(0.8);
}
#[tokio::test]
async fn test_openai_compatible_http_config() {
println!("๐งช Testing OpenAI-compatible HTTP configuration");
let http_config = HttpConfig {
timeout: Some(Duration::from_secs(60)),
connect_timeout: Some(Duration::from_secs(10)),
user_agent: Some("test-agent/1.0".to_string()),
..Default::default()
};
let mut http_config = http_config;
let mut headers = std::collections::HashMap::new();
headers.insert("X-Custom-Header".to_string(), "test-value".to_string());
headers.insert("X-Request-ID".to_string(), "test-123".to_string());
http_config.headers = headers;
let result = LlmBuilder::new()
.siliconflow()
.api_key("test-key")
.model("deepseek-chat")
.with_http_config(http_config.clone())
.build()
.await;
match result {
Ok(client) => {
println!(" โ
SiliconFlow client created with HTTP config");
assert_eq!(client.provider_id(), "siliconflow");
assert_eq!(client.model(), "deepseek-chat");
}
Err(e) => {
println!(
" โ
Failed with expected error (not HTTP config related): {}",
e
);
assert!(!e.to_string().contains("HTTP"));
assert!(!e.to_string().contains("timeout"));
}
}
let result = LlmBuilder::new()
.deepseek()
.api_key("test-key")
.with_http_config(http_config)
.build()
.await;
match result {
Ok(client) => {
println!(" โ
DeepSeek client created with HTTP config");
assert_eq!(client.provider_id(), "deepseek");
}
Err(e) => {
println!(
" โ
Failed with expected error (not HTTP config related): {}",
e
);
assert!(!e.to_string().contains("HTTP"));
}
}
}
#[tokio::test]
async fn test_openai_compatible_default_models() {
println!("๐งช Testing default model configuration for OpenAI-compatible providers");
let result = LlmBuilder::new()
.siliconflow()
.api_key("test-key")
.build()
.await;
match result {
Ok(client) => {
println!(" โ
SiliconFlow client created with default model");
assert_eq!(client.model(), "deepseek-ai/DeepSeek-V3.1");
}
Err(e) => {
println!(" โ
Failed with expected error: {}", e);
assert!(!e.to_string().contains("model"));
}
}
let result = LlmBuilder::new()
.openrouter()
.api_key("test-key")
.build()
.await;
match result {
Ok(client) => {
println!(" โ
OpenRouter client created with default model");
assert_eq!(client.model(), "openai/gpt-4o");
}
Err(e) => {
println!(" โ
Failed with expected error: {}", e);
assert!(!e.to_string().contains("model"));
}
}
}
}