use eventsource_stream::Event;
use siumai::providers::anthropic::streaming::AnthropicEventConverter;
use siumai::providers::gemini::streaming::GeminiEventConverter;
use siumai::providers::ollama::streaming::OllamaEventConverter;
use siumai::providers::openai::streaming::OpenAiEventConverter;
use siumai::stream::ChatStreamEvent;
use siumai::utils::streaming::{JsonEventConverter, SseEventConverter};
#[tokio::test]
async fn test_openai_event_conversion() {
let config = siumai::providers::openai::config::OpenAiConfig::default();
let converter = OpenAiEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: r#"{"choices":[{"delta":{"content":"Hello"}}]}"#.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let content_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ContentDelta { .. })));
if let Some(Ok(ChatStreamEvent::ContentDelta { delta, .. })) = content_event {
assert_eq!(delta, "Hello");
} else {
panic!("Expected ContentDelta event");
}
}
#[tokio::test]
async fn test_anthropic_event_conversion() {
let config = siumai::params::AnthropicParams::default();
let converter = AnthropicEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: r#"{"type":"content_block_delta","delta":{"type":"text_delta","text":"Hello"}}"#
.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let content_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ContentDelta { .. })));
if let Some(Ok(ChatStreamEvent::ContentDelta { delta, .. })) = content_event {
assert_eq!(delta, "Hello");
} else {
panic!("Expected ContentDelta event");
}
}
#[tokio::test]
async fn test_gemini_json_conversion() {
let config = siumai::providers::gemini::types::GeminiConfig::default();
let converter = GeminiEventConverter::new(config);
let json_data = r#"{"candidates":[{"content":{"parts":[{"text":"Hello"}]}}]}"#;
let event = eventsource_stream::Event {
event: "".to_string(),
data: json_data.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let content_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ContentDelta { .. })));
if let Some(Ok(ChatStreamEvent::ContentDelta { delta, .. })) = content_event {
assert_eq!(delta, "Hello");
} else {
panic!("Expected ContentDelta event");
}
}
#[tokio::test]
async fn test_ollama_json_conversion() {
let converter = OllamaEventConverter::new();
let json_data =
r#"{"model":"llama2","message":{"role":"assistant","content":"Hello"},"done":false}"#;
let result = converter.convert_json(json_data).await;
assert!(!result.is_empty());
let content_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ContentDelta { .. })));
if let Some(Ok(ChatStreamEvent::ContentDelta { delta, .. })) = content_event {
assert_eq!(delta, "Hello");
} else {
panic!("Expected ContentDelta event");
}
}
#[tokio::test]
async fn test_openai_thinking_conversion() {
let config = siumai::providers::openai::config::OpenAiConfig::default();
let converter = OpenAiEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: r#"{"choices":[{"delta":{"thinking":"Let me think..."}}]}"#.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let thinking_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ThinkingDelta { .. })));
if let Some(Ok(ChatStreamEvent::ThinkingDelta { delta })) = thinking_event {
assert_eq!(delta, "Let me think...");
} else {
panic!("Expected ThinkingDelta event");
}
}
#[tokio::test]
async fn test_openai_usage_conversion() {
let config = siumai::providers::openai::config::OpenAiConfig::default();
let converter = OpenAiEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: r#"{"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30}}"#
.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let usage_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::UsageUpdate { .. })));
if let Some(Ok(ChatStreamEvent::UsageUpdate { usage })) = usage_event {
assert_eq!(usage.prompt_tokens, 10);
assert_eq!(usage.completion_tokens, 20);
assert_eq!(usage.total_tokens, 30);
} else {
panic!("Expected UsageUpdate event");
}
}
#[tokio::test]
async fn test_openai_content_prioritized_over_usage() {
let config = siumai::providers::openai::config::OpenAiConfig::default();
let converter = OpenAiEventConverter::new(config);
let event = Event {
event: "message".to_string(),
data: r#"{"id":"0198f39ebed63df1a3b0736f167707b4","object":"chat.completion.chunk","created":1756433923,"model":"deepseek-ai/DeepSeek-V3","choices":[{"index":0,"delta":{"content":"` and","reasoning_content":null},"finish_reason":null}],"system_fingerprint":"","usage":{"prompt_tokens":39,"completion_tokens":306,"total_tokens":345}}"#.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let content_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ContentDelta { .. })));
if let Some(Ok(ChatStreamEvent::ContentDelta { delta, index })) = content_event {
assert_eq!(delta, "` and");
assert_eq!(*index, Some(0));
} else {
panic!("Expected ContentDelta event, not UsageUpdate");
}
}
#[tokio::test]
async fn test_xai_content_prioritized_over_usage() {
use siumai::providers::xai::config::XaiConfig;
use siumai::providers::xai::streaming::XaiEventConverter;
let config = XaiConfig::default();
let converter = XaiEventConverter::new(config);
let event = Event {
event: "message".to_string(),
data: r#"{"id":"test-id","object":"chat.completion.chunk","created":1756433923,"model":"grok-beta","choices":[{"index":0,"delta":{"content":"Hello world","reasoning_content":null},"finish_reason":null}],"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15}}"#.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let content_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::ContentDelta { .. })));
if let Some(Ok(ChatStreamEvent::ContentDelta { delta, index })) = content_event {
assert_eq!(delta, "Hello world");
assert_eq!(*index, Some(0));
} else {
panic!("Expected ContentDelta event, not UsageUpdate");
}
}
#[tokio::test]
async fn test_openai_image_generation_capability() {
use siumai::providers::openai::{OpenAiConfig, OpenAiImages};
use siumai::traits::ImageGenerationCapability;
let config = OpenAiConfig::new("test-key");
let images = OpenAiImages::new(config, reqwest::Client::new());
let sizes = images.get_supported_sizes();
assert!(!sizes.is_empty());
assert!(sizes.contains(&"1024x1024".to_string()));
let formats = images.get_supported_formats();
assert!(!formats.is_empty());
assert!(formats.contains(&"url".to_string()));
assert!(images.supports_image_editing());
assert!(images.supports_image_variations());
}
#[tokio::test]
async fn test_siliconflow_image_generation_capability() {
use siumai::providers::openai::{OpenAiConfig, OpenAiImages};
use siumai::traits::ImageGenerationCapability;
let config = OpenAiConfig::new("test-key").with_base_url("https://api.siliconflow.cn/v1");
let images = OpenAiImages::new(config, reqwest::Client::new());
let sizes = images.get_supported_sizes();
assert!(!sizes.is_empty());
assert!(sizes.contains(&"1024x1024".to_string()));
assert!(sizes.contains(&"960x1280".to_string()));
let formats = images.get_supported_formats();
assert_eq!(formats, vec!["url".to_string()]);
assert!(!images.supports_image_editing()); assert!(!images.supports_image_variations()); }
#[tokio::test]
async fn test_openai_finish_reason_conversion() {
let config = siumai::providers::openai::config::OpenAiConfig::default();
let converter = OpenAiEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: r#"{"choices":[{"finish_reason":"stop"}]}"#.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let stream_end_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::StreamEnd { .. })));
if let Some(Ok(ChatStreamEvent::StreamEnd { response })) = stream_end_event {
assert_eq!(
response.finish_reason,
Some(siumai::types::FinishReason::Stop)
);
} else {
println!(
"No StreamEnd event generated for finish_reason-only event: {:?}",
result
);
assert!(!result.is_empty(), "Should generate at least one event");
}
}
#[tokio::test]
async fn test_ollama_stream_end() {
let converter = OllamaEventConverter::new();
let json_data = r#"{"model":"llama2","done":true,"prompt_eval_count":10,"eval_count":20}"#;
let result = converter.convert_json(json_data).await;
assert!(!result.is_empty());
let usage_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::UsageUpdate { .. })));
if let Some(Ok(ChatStreamEvent::UsageUpdate { usage })) = usage_event {
assert_eq!(usage.prompt_tokens, 10);
assert_eq!(usage.completion_tokens, 20);
assert_eq!(usage.total_tokens, 30);
} else {
panic!("Expected UsageUpdate event");
}
}
#[tokio::test]
async fn test_gemini_finish_reason() {
let config = siumai::providers::gemini::types::GeminiConfig::default();
let converter = GeminiEventConverter::new(config);
let json_data = r#"{"candidates":[{"finishReason":"STOP"}]}"#;
let event = eventsource_stream::Event {
event: "".to_string(),
data: json_data.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let stream_end_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::StreamEnd { .. })));
if let Some(Ok(ChatStreamEvent::StreamEnd { response })) = stream_end_event {
assert_eq!(
response.finish_reason,
Some(siumai::types::FinishReason::Stop)
);
} else {
panic!("Expected StreamEnd event");
}
}
#[tokio::test]
async fn test_anthropic_stream_end() {
let config = siumai::params::AnthropicParams::default();
let converter = AnthropicEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: r#"{"type":"message_stop"}"#.to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let stream_end_event = result
.iter()
.find(|event| matches!(event, Ok(ChatStreamEvent::StreamEnd { .. })));
if let Some(Ok(ChatStreamEvent::StreamEnd { response })) = stream_end_event {
assert_eq!(
response.finish_reason,
Some(siumai::types::FinishReason::Stop)
);
} else {
panic!("Expected StreamEnd event");
}
}
#[tokio::test]
async fn test_error_handling() {
let config = siumai::providers::openai::config::OpenAiConfig::default();
let converter = OpenAiEventConverter::new(config);
let event = Event {
event: "".to_string(),
data: "invalid json".to_string(),
id: "".to_string(),
retry: None,
};
let result = converter.convert_event(event).await;
assert!(!result.is_empty());
let error_event = result.iter().find(|event| event.is_err());
if error_event.is_some() {
} else {
panic!("Expected error for invalid JSON");
}
}