use cloudllm::clients::gemini::GeminiClient;
use cloudllm::clients::grok::GrokClient;
use cloudllm::clients::openai::OpenAIClient;
use cloudllm::cloudllm::image_generation::{
decode_base64, get_image_extension_from_base64, register_image_generation_tool,
ImageGenerationClient, ImageGenerationOptions,
};
use cloudllm::init_logger;
use std::fs;
fn api_key_or_skip(env_var: &str) -> Option<String> {
match std::env::var(env_var) {
Ok(value) => Some(value),
Err(_) => {
log::info!("Skipping test: {} not set", env_var);
None
}
}
}
fn is_external_api_error(message: &str) -> bool {
let message = message.to_ascii_lowercase();
if message.contains("for url")
|| message.contains("https://api.openai.com")
|| message.contains("https://api.x.ai")
|| message.contains("https://generativelanguage.googleapis.com")
{
return true;
}
[
"quota",
"resource_exhausted",
"too many requests",
"service unavailable",
"temporarily unavailable",
"error sending request",
"connection reset",
"connection refused",
"connection aborted",
"could not resolve",
"dns",
"timed out",
"timeout",
"network",
"502",
"503",
"504",
"429",
]
.iter()
.any(|needle| message.contains(needle))
}
fn skip_external_error(test_name: &str, error: &dyn std::fmt::Display) -> bool {
let error_message = error.to_string();
if is_external_api_error(&error_message) {
log::info!(
"Skipping {} due to external API/network issue: {}",
test_name,
error_message
);
true
} else {
false
}
}
async fn save_image(image_url_or_b64: &str, filename: &str) -> std::io::Result<()> {
if image_url_or_b64.starts_with("http") {
let data = reqwest::Client::new()
.get(image_url_or_b64)
.send()
.await
.map_err(std::io::Error::other)?
.bytes()
.await
.map_err(std::io::Error::other)?;
fs::write(filename, data)?;
log::info!("Saved image from URL to: {}", filename);
} else {
let decoded = decode_base64(image_url_or_b64).map_err(std::io::Error::other)?;
fs::write(filename, decoded)?;
log::info!("Saved base64 image to: {}", filename);
}
Ok(())
}
#[test]
fn test_gemini_simple() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("1:1".to_string()),
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client.generate_image("A red square", options).await
});
match result {
Ok(response) => {
log::info!("✓ API call succeeded");
log::info!("Response images count: {}", response.images.len());
log::info!("Response revised_prompt: {:?}", response.revised_prompt);
if response.images.is_empty() {
log::error!("❌ ERROR: Response is empty, no images were generated");
panic!("Expected at least one image but response.images is empty");
}
log::info!("✓ Gemini image generation works!");
if let Some(b64) = &response.images[0].b64_json {
log::info!("Base64 length: {} bytes", b64.len());
assert!(!b64.is_empty(), "Base64 data should not be empty");
} else if let Some(url) = &response.images[0].url {
log::info!("Got URL instead: {}", url);
} else {
panic!("Expected base64 or URL data in response");
}
}
Err(e) => {
let error_str = e.to_string();
if error_str.contains("quota") || error_str.contains("RESOURCE_EXHAUSTED") {
log::info!(
"⚠️ Skipping: Gemini free tier quota exhausted. Message: {}",
e
);
} else {
log::error!("❌ Gemini test failed: {}", e);
if skip_external_error("test_gemini_simple", &e) {
return;
}
panic!("Gemini test failed: {}", e);
}
}
}
}
#[test]
fn test_grok_image_generation_basic() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let response_obj = match rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("url".to_string()),
};
client
.generate_image("A serene mountain landscape at sunrise", options)
.await
}) {
Ok(response) => {
log::info!("✓ test_grok_image_generation_basic succeeded");
log::info!("Generated {} images", response.images.len());
assert!(
!response.images.is_empty(),
"Expected at least one generated image"
);
for (idx, image) in response.images.iter().enumerate() {
if let Some(url) = &image.url {
log::info!("Image {}: {}", idx + 1, url);
assert!(url.starts_with("http"), "Expected URL to start with http");
} else {
panic!("Expected image {} to have a URL", idx + 1);
}
}
if let Some(revised) = &response.revised_prompt {
log::info!("Revised prompt: {}", revised);
}
response
}
Err(e) => {
if skip_external_error("test_grok_image_generation_basic", &e) {
return;
}
panic!("test_grok_image_generation_basic failed: {}", e);
}
};
if !response_obj.images.is_empty() {
if let Some(url) = &response_obj.images[0].url {
let rt_save = tokio::runtime::Runtime::new().unwrap();
let filename = "xai_grok_generation_test.png";
if let Err(e) = rt_save.block_on(save_image(url, filename)) {
log::warn!("Failed to save image from URL: {}", e);
} else {
log::info!("✓ Image saved to: {}", filename);
}
} else if let Some(b64) = &response_obj.images[0].b64_json {
let ext = get_image_extension_from_base64(b64);
let filename = format!("xai_grok_generation_test.{}", ext);
let rt_save = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt_save.block_on(save_image(b64, &filename)) {
log::warn!("Failed to save base64 image: {}", e);
} else {
log::info!("✓ Image saved to: {}", filename);
}
}
}
}
#[test]
fn test_grok_image_generation_base64() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image("A robot chef cooking pasta", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_grok_image_generation_base64 succeeded");
assert!(
!response.images.is_empty(),
"Expected at least one generated image"
);
for (idx, image) in response.images.iter().enumerate() {
if let Some(b64) = &image.b64_json {
log::info!(
"Image {} Base64 data (first 50 chars): {}...",
idx + 1,
&b64[..50.min(b64.len())]
);
assert!(!b64.is_empty(), "Expected non-empty base64 data");
} else {
panic!("Expected image {} to have base64_json", idx + 1);
}
}
}
Err(e) => {
if skip_external_error("test_grok_image_generation_base64", &e) {
return;
}
panic!("test_grok_image_generation_base64 failed: {}", e);
}
}
}
#[test]
fn test_grok_image_generation_multiple_images() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(2),
response_format: Some("url".to_string()),
};
client
.generate_image("A futuristic city at sunset with flying cars", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_grok_image_generation_multiple_images succeeded");
assert_eq!(
response.images.len(),
2,
"Expected 2 images, got {}",
response.images.len()
);
for (idx, image) in response.images.iter().enumerate() {
if let Some(url) = &image.url {
log::info!("Variation {}: {}", idx + 1, url);
} else {
panic!("Image {} missing URL", idx + 1);
}
}
}
Err(e) => {
if skip_external_error("test_grok_image_generation_multiple_images", &e) {
return;
}
panic!("test_grok_image_generation_multiple_images failed: {}", e);
}
}
}
#[test]
fn test_grok_image_generation_detailed_prompt() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let detailed_prompt = "A hyper-realistic oil painting of a Japanese garden with a koi pond. \
There's a wooden bridge in the foreground, and cherry blossoms are falling. \
The water is crystal clear and reflects the soft morning light. \
The background shows misty mountains. \
The style is detailed and photorealistic with vibrant colors.";
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("url".to_string()),
};
client.generate_image(detailed_prompt, options).await
});
match result {
Ok(response) => {
log::info!("✓ test_grok_image_generation_detailed_prompt succeeded");
assert!(
!response.images.is_empty(),
"Expected at least one generated image"
);
if let Some(url) = &response.images[0].url {
log::info!("Generated detailed image: {}", url);
}
if let Some(revised) = &response.revised_prompt {
log::info!("Grok revised the prompt to: {}", revised);
}
}
Err(e) => {
if skip_external_error("test_grok_image_generation_detailed_prompt", &e) {
return;
}
panic!("test_grok_image_generation_detailed_prompt failed: {}", e);
}
}
}
#[test]
fn test_grok_image_generation_model_name() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
assert_eq!(
client.model_name(),
"grok-imagine-image",
"Expected image generation model to be grok-imagine-image"
);
log::info!("✓ test_grok_image_generation_model_name succeeded");
log::info!("Image generation model: {}", client.model_name());
}
#[test]
fn test_grok_image_generation_with_trait_object() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
let image_client: &dyn ImageGenerationClient = &client;
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("url".to_string()),
};
image_client
.generate_image("An abstract artwork with vibrant colors", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_grok_image_generation_with_trait_object succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!(
"Successfully used trait object interface, generated {} images",
response.images.len()
);
}
Err(e) => {
if skip_external_error("test_grok_image_generation_with_trait_object", &e) {
return;
}
panic!("test_grok_image_generation_with_trait_object failed: {}", e);
}
}
}
#[test]
fn test_grok_image_generation_with_factory() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client_result = cloudllm::cloudllm::new_image_generation_client(
cloudllm::cloudllm::ImageGenerationProvider::Grok,
&api_key,
);
let client = match client_result {
Ok(c) => c,
Err(e) => panic!("Failed to create client with factory: {}", e),
};
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("url".to_string()),
};
client
.generate_image(
"A serene forest with sunlight streaming through trees",
options,
)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_grok_image_generation_with_factory succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!("Factory-created client model: {}", client.model_name());
}
Err(e) => {
if skip_external_error("test_grok_image_generation_with_factory", &e) {
return;
}
panic!("test_grok_image_generation_with_factory failed: {}", e);
}
}
}
#[test]
fn test_openai_image_generation_basic() {
init_logger();
let Some(api_key) = api_key_or_skip("OPEN_AI_SECRET") else {
return;
};
let client = OpenAIClient::new_with_model_string(&api_key, "gpt-4o-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("4:3".to_string()),
num_images: Some(1),
response_format: Some("url".to_string()),
};
client
.generate_image("A serene mountain landscape at sunrise", options)
.await
});
let response_obj = match result {
Ok(response) => {
log::info!("✓ test_openai_image_generation_basic succeeded");
log::info!("Generated {} images", response.images.len());
assert!(
!response.images.is_empty(),
"Expected at least one generated image"
);
for (idx, image) in response.images.iter().enumerate() {
if let Some(url) = &image.url {
log::info!("Image {} (URL): {}", idx + 1, url);
assert!(url.starts_with("http"), "Expected URL to start with http");
} else if let Some(b64) = &image.b64_json {
log::info!(
"Image {} (Base64, first 50 chars): {}...",
idx + 1,
&b64[..50.min(b64.len())]
);
} else {
panic!(
"Expected image {} to have either URL or base64 data",
idx + 1
);
}
}
response
}
Err(e) => {
if skip_external_error("test_openai_image_generation_basic", &e) {
return;
}
panic!("test_openai_image_generation_basic failed: {}", e);
}
};
if !response_obj.images.is_empty() {
if let Some(url) = &response_obj.images[0].url {
let rt_save = tokio::runtime::Runtime::new().unwrap();
let filename = "openai_openai_generation_test.jpg";
if let Err(e) = rt_save.block_on(save_image(url, filename)) {
log::warn!("Failed to save image from URL: {}", e);
}
} else if let Some(b64) = &response_obj.images[0].b64_json {
let ext = get_image_extension_from_base64(b64);
let filename = format!("openai_openai_generation_test.{}", ext);
let rt_save = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt_save.block_on(save_image(b64, &filename)) {
log::warn!("Failed to save base64 image: {}", e);
}
}
}
}
#[test]
fn test_openai_image_generation_landscape() {
init_logger();
let Some(api_key) = api_key_or_skip("OPEN_AI_SECRET") else {
return;
};
let client = OpenAIClient::new_with_model_string(&api_key, "gpt-4o-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("16:9".to_string()), num_images: Some(1),
response_format: Some("url".to_string()),
};
client
.generate_image("Wide panoramic view of the Grand Canyon", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_openai_image_generation_landscape succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!("Successfully generated landscape image with 16:9 aspect ratio");
}
Err(e) => {
if skip_external_error("test_openai_image_generation_landscape", &e) {
return;
}
panic!("test_openai_image_generation_landscape failed: {}", e);
}
}
}
#[test]
fn test_openai_image_generation_multiple() {
init_logger();
let Some(api_key) = api_key_or_skip("OPEN_AI_SECRET") else {
return;
};
let client = OpenAIClient::new_with_model_string(&api_key, "gpt-4o-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(2),
response_format: Some("url".to_string()),
};
client
.generate_image("A futuristic city at sunset with flying cars", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_openai_image_generation_multiple succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!(
"Successfully generated {} image(s) (requested 2)",
response.images.len()
);
}
Err(e) => {
if skip_external_error("test_openai_image_generation_multiple", &e) {
return;
}
panic!("test_openai_image_generation_multiple failed: {}", e);
}
}
}
#[test]
fn test_openai_image_generation_model_name() {
init_logger();
let Some(api_key) = api_key_or_skip("OPEN_AI_SECRET") else {
return;
};
let client = OpenAIClient::new_with_model_string(&api_key, "gpt-4o-mini");
assert_eq!(
client.model_name(),
"gpt-image-2",
"Expected image generation model to be gpt-image-2"
);
log::info!("✓ test_openai_image_generation_model_name succeeded");
log::info!("Image generation model: {}", client.model_name());
}
#[test]
fn test_openai_image_generation_with_factory() {
init_logger();
let Some(api_key) = api_key_or_skip("OPEN_AI_SECRET") else {
return;
};
let client_result = cloudllm::cloudllm::new_image_generation_client(
cloudllm::cloudllm::ImageGenerationProvider::OpenAI,
&api_key,
);
let client = match client_result {
Ok(c) => c,
Err(e) => panic!("Failed to create client with factory: {}", e),
};
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("url".to_string()),
};
client
.generate_image(
"A serene forest with sunlight streaming through trees",
options,
)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_openai_image_generation_with_factory succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!(
"Factory-created OpenAI client model: {}",
client.model_name()
);
}
Err(e) => {
if skip_external_error("test_openai_image_generation_with_factory", &e) {
return;
}
panic!("test_openai_image_generation_with_factory failed: {}", e);
}
}
}
#[test]
fn test_grok_image_generation_error_handling() {
init_logger();
let client = GrokClient::new_with_model_str("invalid_api_key_12345", "grok-3-mini");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("url".to_string()),
};
client.generate_image("A test image", options).await
});
match result {
Ok(_) => {
panic!("Expected error with invalid API key, but got success");
}
Err(e) => {
log::info!("✓ test_grok_image_generation_error_handling succeeded");
log::info!("Got expected error: {}", e);
assert!(
!e.to_string().is_empty(),
"Error message should not be empty"
);
}
}
}
#[test]
fn test_grok_image_generation_types() {
init_logger();
let Some(api_key) = api_key_or_skip("XAI_API_KEY") else {
return;
};
let client = GrokClient::new_with_model_str(&api_key, "grok-3-mini");
let _: &dyn ImageGenerationClient = &client;
assert_eq!(client.model_name(), "grok-imagine-image");
let options = ImageGenerationOptions {
aspect_ratio: Some("16:9".to_string()),
num_images: Some(2),
response_format: Some("url".to_string()),
};
assert_eq!(options.aspect_ratio.as_deref(), Some("16:9"));
assert_eq!(options.num_images, Some(2));
assert_eq!(options.response_format.as_deref(), Some("url"));
log::info!("✓ test_grok_image_generation_types succeeded");
log::info!("All type checks and trait implementations verified");
}
#[test]
fn test_gemini_image_generation_basic() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image("A serene mountain landscape at sunrise", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_basic succeeded");
log::info!("Generated {} images", response.images.len());
assert!(
!response.images.is_empty(),
"Expected at least one generated image"
);
for (idx, image) in response.images.iter().enumerate() {
if let Some(b64) = &image.b64_json {
log::info!(
"Image {} Base64 data (first 50 chars): {}...",
idx + 1,
&b64[..50.min(b64.len())]
);
assert!(!b64.is_empty(), "Expected non-empty base64 data");
} else {
panic!("Expected image {} to have base64_json data", idx + 1);
}
}
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_basic", &e) {
return;
}
panic!("test_gemini_image_generation_basic failed: {}", e);
}
}
}
#[test]
fn test_gemini_image_generation_landscape() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("16:9".to_string()), num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image("Wide panoramic view of the Grand Canyon at sunset", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_landscape succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!("Successfully generated landscape image with 16:9 aspect ratio");
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_landscape", &e) {
return;
}
panic!("test_gemini_image_generation_landscape failed: {}", e);
}
}
}
#[test]
fn test_gemini_image_generation_portrait() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("9:16".to_string()), num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image("A elegant fashion portrait in studio lighting", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_portrait succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!("Successfully generated portrait image with 9:16 aspect ratio");
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_portrait", &e) {
return;
}
panic!("test_gemini_image_generation_portrait failed: {}", e);
}
}
}
#[test]
fn test_gemini_image_generation_square() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("1:1".to_string()), num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image("A perfect circle of colorful soap bubbles", options)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_square succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!("Successfully generated square image with 1:1 aspect ratio");
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_square", &e) {
return;
}
panic!("test_gemini_image_generation_square failed: {}", e);
}
}
}
#[test]
fn test_gemini_image_generation_detailed_prompt() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let detailed_prompt = "A hyper-realistic oil painting of a Japanese garden with a koi pond. \
There's a wooden bridge in the foreground, and cherry blossoms are falling. \
The water is crystal clear and reflects the soft morning light. \
The background shows misty mountains. \
The style is detailed and photorealistic with vibrant colors.";
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("4:3".to_string()),
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client.generate_image(detailed_prompt, options).await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_detailed_prompt succeeded");
assert!(
!response.images.is_empty(),
"Expected at least one generated image"
);
log::info!("Successfully generated image with detailed artistic prompt");
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_detailed_prompt", &e) {
return;
}
panic!("test_gemini_image_generation_detailed_prompt failed: {}", e);
}
}
}
#[test]
fn test_gemini_image_generation_model_name() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
assert_eq!(
client.model_name(),
"gemini-2.5-flash-image",
"Expected image generation model to be gemini-2.5-flash-image"
);
log::info!("✓ test_gemini_image_generation_model_name succeeded");
log::info!("Image generation model: {}", client.model_name());
}
#[test]
fn test_gemini_image_generation_with_trait_object() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let image_client: &dyn ImageGenerationClient = &client;
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
image_client
.generate_image(
"An abstract artwork with vibrant colors and flowing forms",
options,
)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_with_trait_object succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!(
"Successfully used trait object interface, generated {} images",
response.images.len()
);
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_with_trait_object", &e) {
return;
}
panic!(
"test_gemini_image_generation_with_trait_object failed: {}",
e
);
}
}
}
#[test]
fn test_gemini_image_generation_with_factory() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client_result = cloudllm::cloudllm::new_image_generation_client(
cloudllm::cloudllm::ImageGenerationProvider::Gemini,
&api_key,
);
let client = match client_result {
Ok(c) => c,
Err(e) => panic!("Failed to create client with factory: {}", e),
};
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("3:2".to_string()),
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image(
"A serene forest with sunlight streaming through ancient trees",
options,
)
.await
});
match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_with_factory succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
log::info!(
"Factory-created Gemini client model: {}",
client.model_name()
);
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_with_factory", &e) {
return;
}
panic!("test_gemini_image_generation_with_factory failed: {}", e);
}
}
}
#[test]
fn test_gemini_image_generation_save_to_file() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: Some("3:4".to_string()),
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client
.generate_image(
"A vibrant digital painting of a neon-lit cyberpunk city",
options,
)
.await
});
let response_obj = match result {
Ok(response) => {
log::info!("✓ test_gemini_image_generation_save_to_file succeeded");
assert!(!response.images.is_empty(), "Expected at least one image");
response
}
Err(e) => {
if skip_external_error("test_gemini_image_generation_save_to_file", &e) {
return;
}
panic!("test_gemini_image_generation_save_to_file failed: {}", e);
}
};
if !response_obj.images.is_empty() {
if let Some(b64) = &response_obj.images[0].b64_json {
let ext = get_image_extension_from_base64(b64);
let filename = format!("gemini_generation_test.{}", ext);
let rt_save = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt_save.block_on(save_image(b64, &filename)) {
log::warn!("Failed to save base64 image: {}", e);
} else {
log::info!("Successfully saved Gemini-generated image to: {}", filename);
}
}
}
}
#[test]
fn test_gemini_image_generation_types() {
init_logger();
let Some(api_key) = api_key_or_skip("GEMINI_API_KEY") else {
return;
};
let client = GeminiClient::new_with_model_string(&api_key, "gemini-2.5-flash-image");
let _: &dyn ImageGenerationClient = &client;
assert_eq!(client.model_name(), "gemini-2.5-flash-image");
let ratios = vec![
"1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "21:9",
];
for ratio in ratios {
let options = ImageGenerationOptions {
aspect_ratio: Some(ratio.to_string()),
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
assert_eq!(options.aspect_ratio.as_deref(), Some(ratio));
assert_eq!(options.num_images, Some(1));
assert_eq!(options.response_format.as_deref(), Some("b64_json"));
}
log::info!("✓ test_gemini_image_generation_types succeeded");
log::info!("All type checks and trait implementations verified for Gemini");
}
#[test]
fn test_gemini_image_generation_error_handling() {
init_logger();
let client =
GeminiClient::new_with_model_string("invalid_api_key_12345", "gemini-2.5-flash-image");
let rt = tokio::runtime::Runtime::new().unwrap();
let result = rt.block_on(async {
let options = ImageGenerationOptions {
aspect_ratio: None,
num_images: Some(1),
response_format: Some("b64_json".to_string()),
};
client.generate_image("A test image", options).await
});
match result {
Ok(_) => {
panic!("Expected error with invalid API key, but got success");
}
Err(e) => {
log::info!("✓ test_gemini_image_generation_error_handling succeeded");
log::info!("Got expected error: {}", e);
assert!(
!e.to_string().is_empty(),
"Error message should not be empty"
);
}
}
}
#[test]
fn test_agent_with_image_generation_tool() {
use cloudllm::clients::openai::{Model, OpenAIClient};
use cloudllm::cloudllm::{new_image_generation_client, ImageGenerationProvider};
use cloudllm::tool_protocol::{ToolProtocol, ToolRegistry};
use cloudllm::tool_protocols::CustomToolProtocol;
use cloudllm::Agent;
use std::sync::Arc;
init_logger();
let api_key = std::env::var("OPEN_AI_SECRET");
if api_key.is_err() {
log::info!("⚠️ Skipping: OPEN_AI_SECRET not set");
return;
}
let api_key = api_key.unwrap();
let image_client_result =
new_image_generation_client(ImageGenerationProvider::OpenAI, &api_key);
match image_client_result {
Ok(image_client) => {
let protocol = Arc::new(CustomToolProtocol::new());
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(register_image_generation_tool(
&protocol,
image_client.clone(),
)) {
log::error!("Failed to register image generation tool: {}", e);
panic!("Could not register image generation tool: {}", e);
}
let registry = ToolRegistry::new(protocol.clone());
let agent = Agent::new(
"designer",
"Image Designer Agent",
Arc::new(OpenAIClient::new_with_model_enum(
&api_key,
Model::GPT41Mini,
)),
)
.with_tools(registry)
.with_expertise("Creating visual content")
.with_personality("Creative and detailed");
log::info!("✓ Agent created with image generation tool");
let rt = tokio::runtime::Runtime::new().unwrap();
let system_prompt = "You are a creative image designer. You MUST use the generate_image tool to create images. When the user asks you to generate an image, you MUST call the generate_image tool with a detailed, artistic prompt. Do not just describe the image - actually use the tool.";
let user_message = "Generate an image of a scary clown sitting in an empty classroom. The atmosphere should be eerie and unsettling.";
let result = rt.block_on(async {
agent
.generate(
system_prompt,
user_message,
&[], )
.await
});
match result {
Ok(response) => {
log::info!("✓ Agent response received (length: {})", response.len());
log::debug!("Full response: {}", response);
log::info!(
"Calling image generation tool directly to verify and save image..."
);
let rt_tool = tokio::runtime::Runtime::new().unwrap();
let tool_result = rt_tool.block_on(async {
protocol.execute(
"generate_image",
serde_json::json!({
"prompt": "A scary clown sitting in an empty classroom, eerie unsettling atmosphere, sinister smile, dark menacing eyes, dimly lit with shadows, old desks scattered around, dark color scheme with hints of red",
"aspect_ratio": "16:9"
}),
).await
});
match tool_result {
Ok(tool_result) => {
log::info!("✓ Tool executed successfully");
log::info!("Tool result: {:?}", tool_result);
if let Some(url) =
tool_result.output.get("url").and_then(|u| u.as_str())
{
log::info!("✓ Got image URL from tool");
let rt_save = tokio::runtime::Runtime::new().unwrap();
let filename = "openai_agent_scary_clown.png";
match rt_save.block_on(save_image(url, filename)) {
Ok(_) => {
log::info!("✓✓ Image successfully saved to: {}", filename);
}
Err(e) => {
log::error!("Failed to save image: {}", e);
if skip_external_error(
"test_agent_with_image_generation_tool",
&e,
) {
return;
}
panic!("Could not save image: {}", e);
}
}
} else if let Some(b64) =
tool_result.output.get("b64_json").and_then(|b| b.as_str())
{
log::info!("✓ Got base64 image data from tool");
let ext = get_image_extension_from_base64(b64);
let filename = format!("openai_agent_scary_clown.{}", ext);
let rt_save = tokio::runtime::Runtime::new().unwrap();
match rt_save.block_on(save_image(b64, &filename)) {
Ok(_) => {
log::info!("✓✓ Image successfully saved to: {}", filename);
}
Err(e) => {
log::error!("Failed to save image: {}", e);
if skip_external_error(
"test_agent_with_image_generation_tool",
&e,
) {
return;
}
panic!("Could not save image: {}", e);
}
}
} else {
log::error!(
"Tool result doesn't contain URL or base64: {:?}",
tool_result.output
);
panic!("Tool result missing URL or base64 field");
}
}
Err(e) => {
log::error!("Tool execution failed: {}", e);
if skip_external_error("test_agent_with_image_generation_tool", &e) {
return;
}
panic!("Could not execute image generation tool: {}", e);
}
}
}
Err(e) => {
if skip_external_error("test_agent_with_image_generation_tool", &e) {
return;
}
panic!("Agent generation failed: {}", e);
}
}
}
Err(e) => {
if skip_external_error("test_agent_with_image_generation_tool", &e) {
return;
}
panic!("Failed to create image generation client: {}", e);
}
}
}
#[test]
fn test_grok_agent_with_image_generation_tool() {
use cloudllm::clients::grok::{GrokClient, Model};
use cloudllm::cloudllm::{new_image_generation_client, ImageGenerationProvider};
use cloudllm::tool_protocol::{ToolProtocol, ToolRegistry};
use cloudllm::tool_protocols::CustomToolProtocol;
use cloudllm::Agent;
use std::sync::Arc;
init_logger();
let api_key = std::env::var("XAI_API_KEY");
if api_key.is_err() {
log::info!("⚠️ Skipping: XAI_API_KEY not set");
return;
}
let api_key = api_key.unwrap();
let image_client_result = new_image_generation_client(ImageGenerationProvider::Grok, &api_key);
match image_client_result {
Ok(image_client) => {
let protocol = Arc::new(CustomToolProtocol::new());
let rt = tokio::runtime::Runtime::new().unwrap();
if let Err(e) = rt.block_on(register_image_generation_tool(
&protocol,
image_client.clone(),
)) {
log::error!("Failed to register image generation tool: {}", e);
panic!("Could not register image generation tool: {}", e);
}
let registry = ToolRegistry::new(protocol.clone());
let agent = Agent::new(
"grok_designer",
"Grok Image Designer Agent",
Arc::new(GrokClient::new_with_model_enum(&api_key, Model::Grok3Mini)),
)
.with_tools(registry)
.with_expertise("Creating visual content with Grok")
.with_personality("Creative and detailed");
log::info!("✓ Grok agent created with image generation tool");
let rt = tokio::runtime::Runtime::new().unwrap();
let system_prompt = "You are a creative image designer using Grok. You MUST use the generate_image tool to create images. When the user asks you to generate an image, you MUST call the generate_image tool with a detailed, artistic prompt. Do not just describe the image - actually use the tool.";
let user_message = "Generate an image of a scary clown sitting in an empty classroom. The atmosphere should be eerie and unsettling.";
let result = rt.block_on(async {
agent
.generate(
system_prompt,
user_message,
&[], )
.await
});
match result {
Ok(response) => {
log::info!(
"✓ Grok agent response received (length: {})",
response.len()
);
log::debug!("Full response: {}", response);
log::info!(
"Calling Grok image generation tool directly to verify and save image..."
);
let rt_tool = tokio::runtime::Runtime::new().unwrap();
let tool_result = rt_tool.block_on(async {
protocol.execute(
"generate_image",
serde_json::json!({
"prompt": "A scary clown sitting in an empty classroom, eerie unsettling atmosphere, sinister smile, dark menacing eyes, dimly lit with shadows, old desks scattered around, dark color scheme with hints of red",
"aspect_ratio": "16:9"
}),
).await
});
match tool_result {
Ok(tool_result) => {
log::info!("✓ Grok tool executed successfully");
log::info!("Grok tool result: {:?}", tool_result);
if let Some(url) =
tool_result.output.get("url").and_then(|u| u.as_str())
{
log::info!("✓ Got image URL from Grok tool");
let rt_save = tokio::runtime::Runtime::new().unwrap();
let filename = "xai_agent_scary_clown.png";
match rt_save.block_on(save_image(url, filename)) {
Ok(_) => {
log::info!(
"✓✓ Grok image successfully saved to: {}",
filename
);
}
Err(e) => {
log::error!("Failed to save Grok image: {}", e);
if skip_external_error(
"test_grok_agent_with_image_generation_tool",
&e,
) {
return;
}
panic!("Could not save image: {}", e);
}
}
} else if let Some(b64) =
tool_result.output.get("b64_json").and_then(|b| b.as_str())
{
log::info!("✓ Got base64 image data from Grok tool");
let ext = get_image_extension_from_base64(b64);
let filename = format!("xai_agent_scary_clown.{}", ext);
let rt_save = tokio::runtime::Runtime::new().unwrap();
match rt_save.block_on(save_image(b64, &filename)) {
Ok(_) => {
log::info!(
"✓✓ Grok image successfully saved to: {}",
filename
);
}
Err(e) => {
log::error!("Failed to save Grok image: {}", e);
if skip_external_error(
"test_grok_agent_with_image_generation_tool",
&e,
) {
return;
}
panic!("Could not save image: {}", e);
}
}
} else {
log::error!(
"Grok tool result doesn't contain URL or base64: {:?}",
tool_result.output
);
panic!("Tool result missing URL or base64 field");
}
}
Err(e) => {
log::error!("Grok tool execution failed: {}", e);
if skip_external_error("test_grok_agent_with_image_generation_tool", &e)
{
return;
}
panic!("Could not execute image generation tool: {}", e);
}
}
}
Err(e) => {
if skip_external_error("test_grok_agent_with_image_generation_tool", &e) {
return;
}
panic!("Grok agent generation failed: {}", e);
}
}
}
Err(e) => {
if skip_external_error("test_grok_agent_with_image_generation_tool", &e) {
return;
}
panic!("Failed to create Grok image client: {}", e);
}
}
}