Compare commits

...
Author SHA1 Message Date
Brandon Thomas 3581d2c698 extract image, more tests 2026-05-11 21:11:01 -04:00
Brandon Thomas 85c20cd3f9 tests 2026-05-11 20:51:34 -04:00
Brandon Thomas bda1060a21 poll response 2026-05-11 20:41:37 -04:00
Brandon Thomas ed1c7fbc1b new error types 2026-05-11 19:55:42 -04:00
Brandon Thomas aded57c9a0 polling 2026-05-11 19:40:19 -04:00
Brandon Thomas 8032b79a4d 3d 2026-05-11 19:05:45 -04:00
Brandon Thomas 9937efed81 3d 2026-05-11 19:01:14 -04:00
Brandon Thomas 8a2c4dc0e8 kling 3.0 i2v 2026-05-11 18:29:25 -04:00
Brandon Thomas 09e197d19a add kling video 2026-05-11 18:25:40 -04:00
Brandon Thomas bfc34005d9 basis for polling 2026-05-11 18:20:59 -04:00
Brandon Thomas 3543393965 test 2026-05-11 17:42:28 -04:00
Brandon Thomas d2d56d7220 polling 2026-05-11 17:09:30 -04:00
Brandon Thomas 25b2be92c8 artcraft router: image pipeline v2 w/ some fal models for testing (#1500) 2026-05-11 12:24:42 -04:00
BFlat 89f6015e3f Fix aspect ratio picker losing label after model switch (#1499)
When the stored `commonAspectRatio` (e.g. `SquareHd`) isn't in the
newly-selected model's `aspectRatios`, `PopoverMenu` toggle mode found
no selected item and collapsed the trigger to icon-only.

Reset to `model.defaultAspectRatio` from inside `AspectRatioPicker` so
the fix applies to all three callers (image/edit/3D). Drops the
duplicate workaround from `PromptBox3D`.
2026-05-11 20:24:01 +07:00
BFlat 3b889b6fdb website: make video button (#1498)
- added make video button to the create image page gallery card action
button row and the lightbox modal
- clicking it will redirect to the create video page and insert the
image as a reference automatically
2026-05-11 18:59:11 +07:00
Brandon Thomas 3ab2974d89 new fal client design to handle webhooks + queued requests with same API (#1497) 2026-05-10 07:48:13 -04:00
Brandon Thomas 220cf66dfc fal_client refactors ahead of a bigger one (#1496) 2026-05-09 19:46:32 -04:00
Tanish Manku 2d18a07843 3D PageScene: Componentize and big refactor (#1492) 2026-05-09 14:26:42 -07:00
Tanish Manku 369c552096 PageDraw: Generate fixes and performance boost (#1494) 2026-05-09 13:51:25 -07:00
Brandon Thomas 13a5dad3a5 beeble improvements, don't 500 on bad input (#1493) 2026-05-09 10:49:05 -04:00
bflatastic f8be6ea4c9 website: hide resolution picker, progress timer 5 mins 2026-05-09 16:35:18 +07:00
bflatastic 5f70776afe website: unified credits chip for mobile and desktop view 2026-05-09 09:28:23 +07:00
bflatastic 5c5a7d1182 credits in top navbar mobile 2026-05-09 09:12:20 +07:00
Brandon Thomas d0e5a05253 frontend api stuff (#1491) 2026-05-08 22:01:21 -04:00
Brandon Thomas 72e052fe7e add redirects for netlify 2026-05-08 21:44:01 -04:00
Brandon Thomas 7471259d28 fix frontend errors (missing vite config, type errors) (#1490) 2026-05-08 21:40:46 -04:00
822 changed files with 19089 additions and 48803 deletions
@@ -5,5 +5,6 @@ pub mod request_mismatch_mitigation_strategy;
pub mod router_artcraft_client;
pub mod router_client;
pub mod router_fal_client;
pub mod router_fal_webhook_optional_client;
pub mod router_muapi_client;
pub mod router_seedance2pro_client;
@@ -1,5 +1,6 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -7,6 +8,7 @@ use crate::errors::client_error::{ClientError, ClientType};
pub struct MultiRouterClient {
pub(crate) artcraft_client: Option<RouterArtcraftClient>,
pub(crate) fal_client: Option<RouterFalClient>,
pub(crate) fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
pub(crate) muapi_client: Option<RouterMuapiClient>,
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
}
@@ -21,6 +23,10 @@ impl MultiRouterClient {
self.fal_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Fal))
}
pub fn get_fal_webhook_optional_client_ref(&self) -> Result<&RouterFalWebhookOptionalClient, ClientError> {
self.fal_webhook_optional_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::FalWebhookOptional))
}
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
self.muapi_client.as_ref()
@@ -1,12 +1,14 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
pub struct MultiRouterClientBuilder {
artcraft_client: Option<RouterArtcraftClient>,
fal_client: Option<RouterFalClient>,
fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
muapi_client: Option<RouterMuapiClient>,
seedance2pro_client: Option<RouterSeedance2ProClient>,
}
@@ -16,6 +18,7 @@ impl MultiRouterClientBuilder {
Self {
artcraft_client: None,
fal_client: None,
fal_webhook_optional_client: None,
muapi_client: None,
seedance2pro_client: None,
}
@@ -30,6 +33,11 @@ impl MultiRouterClientBuilder {
self.fal_client = Some(client);
self
}
pub fn set_fal_webhook_optional_client(mut self, client: RouterFalWebhookOptionalClient) -> Self {
self.fal_webhook_optional_client = Some(client);
self
}
pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self {
self.muapi_client = Some(client);
@@ -45,6 +53,7 @@ impl MultiRouterClientBuilder {
MultiRouterClient {
artcraft_client: self.artcraft_client,
fal_client: self.fal_client,
fal_webhook_optional_client: self.fal_webhook_optional_client,
muapi_client: self.muapi_client,
seedance2pro_client: self.seedance2pro_client,
}
@@ -1,6 +1,7 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -9,6 +10,7 @@ pub enum RouterClient {
Multi(MultiRouterClient),
Artcraft(RouterArtcraftClient),
Fal(RouterFalClient),
FalWebhookOptional(RouterFalWebhookOptionalClient),
Muapi(RouterMuapiClient),
Seedance2Pro(RouterSeedance2ProClient),
}
@@ -30,6 +32,14 @@ impl RouterClient {
}
}
pub fn get_fal_webhook_optional_client_ref(&self) -> Result<&RouterFalWebhookOptionalClient, ClientError> {
match self {
RouterClient::FalWebhookOptional(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_fal_webhook_optional_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Fal)),
}
}
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
match self {
RouterClient::Muapi(client) => Ok(client),
@@ -0,0 +1,19 @@
use fal_client::creds::fal_api_key::FalApiKey;
pub struct RouterFalWebhookOptionalClient {
pub(crate) api_key: FalApiKey,
pub(crate) webhook_url: Option<String>,
}
impl RouterFalWebhookOptionalClient {
pub fn new(api_key: FalApiKey) -> Self {
Self { api_key, webhook_url: None }
}
pub fn new_with_webhook(api_key: FalApiKey, webhook_url: String) -> Self {
Self {
api_key,
webhook_url: Some(webhook_url)
}
}
}
@@ -7,6 +7,7 @@ use tokens::tokens::media_files::MediaFileToken;
pub enum ClientType {
Artcraft,
Fal,
FalWebhookOptional,
Muapi,
Seedance2Pro,
}
@@ -16,6 +17,7 @@ impl Display for ClientType {
match self {
Self::Artcraft => write!(f, "Artcraft"),
Self::Fal => write!(f, "Fal"),
Self::FalWebhookOptional => write!(f, "FalWebhookOptional"),
Self::Muapi => write!(f, "Muapi"),
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
}
@@ -33,10 +33,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Dev,
provider: Provider::Artcraft,
prompt: None,
@@ -20,11 +20,11 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
#[test]
fn test_estimate_cost_is_none() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Schnell,
provider: Provider::Artcraft,
prompt: None,
@@ -34,12 +34,12 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use tokens::tokens::media_files::MediaFileToken;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let tokens = vec![MediaFileToken::new_from_str("test_token")];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Flux2LoraAngles,
provider: Provider::Artcraft,
prompt: None,
@@ -36,10 +36,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::FluxPro11,
provider: Provider::Artcraft,
prompt: None,
@@ -33,10 +33,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::FluxPro11Ultra,
provider: Provider::Artcraft,
prompt: None,
@@ -59,11 +59,11 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use tokens::tokens::media_files::MediaFileToken;
fn base() -> GenerateImageRequest {
GenerateImageRequest {
fn base() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1,
provider: Provider::Artcraft,
prompt: None,
@@ -86,7 +86,7 @@ mod tests {
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality,
aspect_ratio,
image_batch_count: Some(batch),
@@ -105,7 +105,7 @@ mod tests {
let tokens: Vec<MediaFileToken> = (0..num_input_images)
.map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i)))
.collect();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality,
aspect_ratio,
image_batch_count: Some(batch),
@@ -64,14 +64,14 @@ mod tests {
use crate::api::common_quality::CommonQuality;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1p5,
provider: Provider::Artcraft,
prompt: None,
@@ -33,10 +33,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::NanoBanana,
provider: Provider::Artcraft,
prompt: None,
@@ -46,10 +46,10 @@ mod tests {
use crate::api::common_resolution::CommonResolution;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::NanoBanana2,
provider: Provider::Artcraft,
prompt: None,
@@ -40,10 +40,10 @@ mod tests {
use crate::api::common_resolution::CommonResolution;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Artcraft,
prompt: None,
@@ -34,12 +34,12 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use tokens::tokens::media_files::MediaFileToken;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let tokens = vec![MediaFileToken::new_from_str("test_token")];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::QwenEdit2511Angles,
provider: Provider::Artcraft,
prompt: None,
@@ -33,10 +33,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Seedream4,
provider: Provider::Artcraft,
prompt: None,
@@ -33,10 +33,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Seedream4p5,
provider: Provider::Artcraft,
prompt: None,
@@ -33,10 +33,10 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Seedream5Lite,
provider: Provider::Artcraft,
prompt: None,
@@ -38,10 +38,10 @@ mod tests {
use crate::api::common_resolution::CommonResolution;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Fal,
prompt: None,
@@ -36,7 +36,7 @@ pub async fn execute_artcraft_flux_1_dev(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_1_dev_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -45,7 +45,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_dev() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -68,7 +68,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_dev_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
@@ -94,7 +94,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -36,7 +36,7 @@ pub async fn execute_artcraft_flux_1_schnell(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::test_helpers::{base_flux_1_schnell_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -44,7 +44,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_schnell() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -67,7 +67,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_schnell_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
@@ -93,7 +93,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -39,7 +39,7 @@ pub async fn execute_artcraft_flux_2_lora_angles(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_2_lora_angles_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
@@ -49,7 +49,7 @@ mod tests {
async fn test_angle_edit_flux_2_lora() {
let client = get_artcraft_client();
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
quality: None,
image_batch_count: Some(1),
@@ -36,7 +36,7 @@ pub async fn execute_artcraft_flux_pro_1p1(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_pro_1p1_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -45,7 +45,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -68,7 +68,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
@@ -94,7 +94,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -36,7 +36,7 @@ pub async fn execute_artcraft_flux_pro_1p1_ultra(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_pro_1p1_ultra_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -45,7 +45,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_ultra() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -68,7 +68,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_ultra_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
@@ -94,7 +94,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -64,7 +64,7 @@ pub async fn execute_artcraft_gpt_image_1(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_gpt_image_1_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
@@ -73,7 +73,7 @@ mod tests {
// (no network I/O — we never call generate_image).
#[test]
fn build_text_to_image_plan_smoke() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_gpt_image_1_image_request()
@@ -88,7 +88,7 @@ mod tests {
#[test]
fn build_edit_image_plan_smoke() {
let tokens = vec![MediaFileToken::new_from_str("mf_test00000000000000000000000000")];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_gpt_image_1_image_request()
};
@@ -103,7 +103,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_1() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
@@ -128,7 +128,7 @@ mod tests {
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a desert with cacti".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Square),
@@ -43,7 +43,7 @@ mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_gpt_image_1p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -53,7 +53,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_1p5() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
@@ -80,7 +80,7 @@ mod tests {
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a desert with cacti".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto),
@@ -106,7 +106,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a bear walking through a abandoned desert city at night".to_string()),
@@ -139,7 +139,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to an alien world".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
@@ -48,13 +48,13 @@ mod tests {
use crate::api::common_quality::CommonQuality;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{get_artcraft_client, get_artcraft_client_for_env};
use tokens::tokens::media_files::MediaFileToken;
fn base_gpt_image_2_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_gpt_image_2_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: crate::api::common_image_model::CommonImageModel::GptImage2,
provider: crate::api::provider::Provider::Artcraft,
prompt: Some("a t-rex walking through a cyberpunk city at night".to_string()),
@@ -76,7 +76,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_2() {
let client = get_artcraft_client_for_env(ApiHost::Localhost { port: 12345 });
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
..base_gpt_image_2_request()
@@ -102,7 +102,7 @@ mod tests {
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a desert with cacti".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
@@ -125,7 +125,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_2_high_quality() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: Some(CommonQuality::High),
image_batch_count: Some(1),
@@ -38,7 +38,7 @@ mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_nano_banana_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -48,7 +48,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -76,7 +76,7 @@ mod tests {
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto),
@@ -103,7 +103,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -137,7 +137,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
@@ -38,7 +38,7 @@ pub async fn execute_artcraft_nano_banana_2(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_nano_banana_2_image_request, get_artcraft_client};
@@ -46,7 +46,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_2() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
quality: None,
@@ -39,7 +39,7 @@ mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
@@ -48,7 +48,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_pro() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
quality: None,
@@ -72,7 +72,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_pro_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
quality: None,
@@ -103,7 +103,7 @@ mod tests {
MediaFileToken::new_from_str("m_2220d5we3masng5hvzg5zz68ywbdqd"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Add the man in the first image into the fantasy scene in the second image. Keep his hoodie and attire, but match the vibe of the fantasy scene.".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
@@ -40,7 +40,7 @@ pub async fn execute_artcraft_qwen_edit_2511_angles(
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_qwen_edit_2511_angles_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
@@ -50,7 +50,7 @@ mod tests {
async fn test_angle_edit_qwen_edit_2511() {
let client = get_artcraft_client();
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
quality: None,
image_batch_count: Some(1),
@@ -39,7 +39,7 @@ mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -49,7 +49,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -77,7 +77,7 @@ mod tests {
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto),
@@ -104,7 +104,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -138,7 +138,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
@@ -39,7 +39,7 @@ mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -49,7 +49,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4p5() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -77,7 +77,7 @@ mod tests {
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a spooky haunted house".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto2k),
@@ -104,7 +104,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
@@ -138,7 +138,7 @@ mod tests {
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a spooky haunted house".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
@@ -37,7 +37,7 @@ pub async fn execute_artcraft_seedream_5_lite(
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_5_lite_image_request, get_artcraft_client};
@@ -45,7 +45,7 @@ mod tests {
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_5_lite() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
@@ -79,12 +79,12 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::get_fal_client;
fn base_fal_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_fal_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
@@ -105,7 +105,7 @@ mod tests {
// Build-only smoke test (no network I/O).
#[test]
fn build_text_to_image_plan_smoke() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_fal_request()
@@ -121,7 +121,7 @@ mod tests {
#[test]
fn build_edit_image_plan_smoke() {
let urls = vec!["https://example.com/img.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_fal_request()
};
@@ -136,7 +136,7 @@ mod tests {
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn test_text_to_image_gpt_image_1_fal() {
let client = get_fal_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
@@ -158,7 +158,7 @@ mod tests {
let urls = vec![
TREX_SKELETON_IMAGE_URL.to_string(),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
@@ -76,7 +76,7 @@ mod tests {
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::get_fal_client;
@@ -87,7 +87,7 @@ mod tests {
#[test]
fn text_to_image_plan() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_request()
@@ -105,7 +105,7 @@ mod tests {
JUNO_AT_LAKE_IMAGE_URL.to_string(),
GHOST_IMAGE_URL.to_string(),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("dog running from a scary ghost".to_string()),
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_request()
@@ -127,7 +127,7 @@ mod tests {
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn text_to_image() {
let client = get_fal_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
@@ -147,7 +147,7 @@ mod tests {
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn edit_image() {
let client = get_fal_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("dog running from a scary ghost".to_string()),
image_inputs: Some(ImageListRef::Urls(vec![
JUNO_AT_LAKE_IMAGE_URL.to_string(),
@@ -170,8 +170,8 @@ mod tests {
// ── Helpers ──
fn base_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage2,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
@@ -109,7 +109,7 @@ mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_fal_image_request, get_fal_client};
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
@@ -118,7 +118,7 @@ mod tests {
#[ignore] // manually run — fires a real Fal API request
async fn test_text_to_image_fal_nano_banana_pro() {
let client = get_fal_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::TwoK),
quality: None,
@@ -143,7 +143,7 @@ mod tests {
#[ignore] // manually run — fires a real Fal API request
async fn test_text_to_image_fal_nano_banana_pro_batch_four() {
let client = get_fal_client();
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
quality: None,
@@ -174,7 +174,7 @@ mod tests {
TREX_SKELETON_IMAGE_URL.to_string(),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
prompt: Some("Add the ghost from the first image hovering above the T-Rex skeleton in the second image, make it look spooky but friendly".to_string()),
image_inputs: Some(ImageListRef::Urls(image_urls.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
@@ -9,6 +9,11 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::plan_generate_image_artcraft_flux_1_schnell;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
@@ -39,7 +44,7 @@ use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_ban
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
#[derive(Clone, Debug)]
pub struct GenerateImageRequest {
pub struct GenerateImageRequestBuilder {
/// Which model to use.
pub model: CommonImageModel,
@@ -88,7 +93,28 @@ pub struct GenerateImageRequest {
pub idempotency_token: Option<String>,
}
impl GenerateImageRequest {
impl GenerateImageRequestBuilder {
pub fn use_new_builder(&self) -> bool {
match (self.provider, self.model) {
(Provider::Fal, CommonImageModel::Flux1Dev) => true,
(Provider::Fal, CommonImageModel::Flux1Schnell) => true,
(Provider::Fal, CommonImageModel::NanoBanana2) => true,
(Provider::Fal, CommonImageModel::NanoBananaPro) => true,
_ => false,
}
}
pub fn build2(self) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
match (self.provider, self.model) {
(Provider::Fal, CommonImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
(Provider::Fal, CommonImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
(Provider::Fal, CommonImageModel::NanoBanana2) => build_fal_nano_banana_2(self),
(Provider::Fal, CommonImageModel::NanoBananaPro) => build_fal_nano_banana_pro(self),
_ => self.unsupported_provider_and_model(),
}
}
/// Read the image generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
match self.provider {
@@ -149,6 +175,12 @@ impl GenerateImageRequest {
))
}
fn unsupported_provider_and_model(&self) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(
format!("Image generation for model `{:?}` is not supported for provider {:?} using the new API", self.model, self.provider)
))
}
pub fn get_or_generate_idempotency_token(&self) -> String {
self.idempotency_token.clone()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
@@ -1,6 +1,6 @@
pub mod cost;
pub mod execute;
pub mod generate_image_request;
pub mod generate_image_request_builder;
pub mod generate_image_response;
pub mod image_generation_cost_estimate;
pub mod image_generation_plan;
@@ -3,7 +3,7 @@ use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStra
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages,
@@ -18,7 +18,7 @@ pub struct PlanArtcraftFlux1Dev {
}
pub fn plan_generate_image_artcraft_flux_1_dev(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
// Flux 1 Dev is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
@@ -134,7 +134,7 @@ mod tests {
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::test_helpers::base_flux_1_dev_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio as FlAr,
@@ -146,7 +146,7 @@ mod tests {
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_1_dev_image_request()
@@ -161,7 +161,7 @@ mod tests {
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_1_dev_image_request()
@@ -172,7 +172,7 @@ mod tests {
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: None,
..base_flux_1_dev_image_request()
@@ -184,7 +184,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_1_dev_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(plan.aspect_ratio.is_none());
}
@@ -192,7 +192,7 @@ mod tests {
#[test]
fn aspect_ratio_auto_falls_back_to_square() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_1_dev_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
@@ -216,7 +216,7 @@ mod tests {
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_1_dev_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -227,7 +227,7 @@ mod tests {
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_dev_image_request()
@@ -245,7 +245,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
@@ -267,7 +267,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -284,7 +284,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_1_dev_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -295,7 +295,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -313,7 +313,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -3,7 +3,7 @@ use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStra
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages,
@@ -18,7 +18,7 @@ pub struct PlanArtcraftFlux1Schnell {
}
pub fn plan_generate_image_artcraft_flux_1_schnell(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
// Flux 1 Schnell is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
@@ -134,7 +134,7 @@ mod tests {
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::test_helpers::base_flux_1_schnell_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio as FlAr,
@@ -146,7 +146,7 @@ mod tests {
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_1_schnell_image_request()
@@ -161,7 +161,7 @@ mod tests {
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_1_schnell_image_request()
@@ -172,7 +172,7 @@ mod tests {
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: None,
..base_flux_1_schnell_image_request()
@@ -184,7 +184,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_1_schnell_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(plan.aspect_ratio.is_none());
}
@@ -192,7 +192,7 @@ mod tests {
#[test]
fn aspect_ratio_auto_falls_back_to_square() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_1_schnell_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
@@ -216,7 +216,7 @@ mod tests {
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_1_schnell_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -227,7 +227,7 @@ mod tests {
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_schnell_image_request()
@@ -244,7 +244,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
@@ -263,7 +263,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -280,7 +280,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_1_schnell_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -291,7 +291,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -309,7 +309,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize,
@@ -23,7 +23,7 @@ pub struct PlanArtcraftFlux2LoraAngles {
}
pub fn plan_generate_image_artcraft_flux_2_lora_angles(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -153,7 +153,7 @@ mod tests {
#[test]
fn requires_image_input() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: None,
..base_flux_2_lora_angles_image_request()
};
@@ -164,7 +164,7 @@ mod tests {
#[test]
fn rejects_urls() {
let urls = vec!["https://example.com/img.png".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_flux_2_lora_angles_image_request()
};
@@ -175,7 +175,7 @@ mod tests {
#[test]
fn accepts_single_media_token() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
@@ -186,7 +186,7 @@ mod tests {
#[test]
fn rejects_multiple_media_tokens() {
let tokens = vec![make_token(), make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
@@ -197,7 +197,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: None,
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
@@ -209,7 +209,7 @@ mod tests {
#[test]
fn image_size_square_maps_directly() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
@@ -223,7 +223,7 @@ mod tests {
let tokens = vec![make_token()];
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
@@ -240,7 +240,7 @@ mod tests {
#[test]
fn angle_fields_are_passed_through() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
horizontal_angle: Some(45.0),
vertical_angle: Some(-30.0),
zoom: Some(1.5),
@@ -3,7 +3,7 @@ use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStra
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages,
@@ -18,7 +18,7 @@ pub struct PlanArtcraftFluxPro11 {
}
pub fn plan_generate_image_artcraft_flux_pro_1p1(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
// Flux Pro 1.1 is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
@@ -134,7 +134,7 @@ mod tests {
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::test_helpers::base_flux_pro_1p1_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
GenerateFluxPro11TextToImageAspectRatio as FlAr,
@@ -146,7 +146,7 @@ mod tests {
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_pro_1p1_image_request()
@@ -161,7 +161,7 @@ mod tests {
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_pro_1p1_image_request()
@@ -172,7 +172,7 @@ mod tests {
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: None,
..base_flux_pro_1p1_image_request()
@@ -184,7 +184,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_pro_1p1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(plan.aspect_ratio.is_none());
}
@@ -197,7 +197,7 @@ mod tests {
CommonAspectRatio::Auto4k,
CommonAspectRatio::SquareHd,
] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_pro_1p1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
@@ -220,7 +220,7 @@ mod tests {
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_pro_1p1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -231,7 +231,7 @@ mod tests {
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_pro_1p1_image_request()
@@ -248,7 +248,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_image_request()
@@ -267,7 +267,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -284,7 +284,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_pro_1p1_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -295,7 +295,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -313,7 +313,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -3,7 +3,7 @@ use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStra
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages,
@@ -18,7 +18,7 @@ pub struct PlanArtcraftFluxPro11Ultra {
}
pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
// Flux Pro 1.1 Ultra is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
@@ -137,7 +137,7 @@ mod tests {
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::test_helpers::base_flux_pro_1p1_ultra_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
GenerateFluxPro11UltraTextToImageAspectRatio as FlAr,
@@ -149,7 +149,7 @@ mod tests {
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_pro_1p1_ultra_image_request()
@@ -164,7 +164,7 @@ mod tests {
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_pro_1p1_ultra_image_request()
@@ -175,7 +175,7 @@ mod tests {
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: None,
..base_flux_pro_1p1_ultra_image_request()
@@ -187,7 +187,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_pro_1p1_ultra_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(plan.aspect_ratio.is_none());
}
@@ -200,7 +200,7 @@ mod tests {
CommonAspectRatio::Auto4k,
CommonAspectRatio::SquareHd,
] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_pro_1p1_ultra_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
@@ -223,7 +223,7 @@ mod tests {
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineByTwentyOne),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_pro_1p1_ultra_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -234,7 +234,7 @@ mod tests {
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_pro_1p1_ultra_image_request()
@@ -251,7 +251,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_ultra_image_request()
@@ -270,7 +270,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -287,7 +287,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_pro_1p1_ultra_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -298,7 +298,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -316,7 +316,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{
GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages,
@@ -60,7 +60,7 @@ pub struct PlanArtcraftGptImage1 {
}
pub fn plan_generate_image_artcraft_gpt_image_1(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -256,7 +256,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -266,7 +266,7 @@ mod tests {
#[test]
fn image_size_auto_variants_yield_none() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(auto_ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -277,7 +277,7 @@ mod tests {
#[test]
fn image_size_square_variants() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -296,7 +296,7 @@ mod tests {
CommonAspectRatio::Wide,
];
for ar in wide_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -315,7 +315,7 @@ mod tests {
CommonAspectRatio::Tall,
];
for ar in tall_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -343,7 +343,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1_image_request()
@@ -358,7 +358,7 @@ mod tests {
#[test]
fn num_images_default_is_one() {
let request = GenerateImageRequest { image_batch_count: None, ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: None, ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -374,7 +374,7 @@ mod tests {
(4, ArtcraftGptImage1NumImages::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -387,7 +387,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_gpt_image_1_image_request()
@@ -405,7 +405,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(9),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1_image_request()
@@ -424,7 +424,7 @@ mod tests {
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -434,7 +434,7 @@ mod tests {
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_gpt_image_1_image_request()
};
@@ -450,7 +450,7 @@ mod tests {
// cost estimation against Artcraft. URLs are accepted (and dropped) since
// cost only depends on quality + size + num_images.
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_gpt_image_1_image_request()
};
@@ -474,7 +474,7 @@ mod tests {
#[test]
fn idempotency_token_passthrough() {
let token = "11111111-1111-1111-1111-111111111111";
let request = GenerateImageRequest { idempotency_token: Some(token.to_string()), ..base_gpt_image_1_image_request() };
let request = GenerateImageRequestBuilder { idempotency_token: Some(token.to_string()), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality,
@@ -25,7 +25,7 @@ pub struct PlanArtcraftGptImage1p5 {
}
pub fn plan_generate_image_artcraft_gpt_image_1p5(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -148,7 +148,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(plan.image_size.is_none());
}
@@ -156,7 +156,7 @@ mod tests {
#[test]
fn image_size_auto_variants_yield_none() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(auto_ar), ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar);
}
@@ -165,7 +165,7 @@ mod tests {
#[test]
fn image_size_square_variants() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.image_size, Some(GS::Square)), "expected Square for {:?}", ar);
}
@@ -182,7 +182,7 @@ mod tests {
CommonAspectRatio::Wide,
];
for ar in wide_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.image_size, Some(GS::Wide)), "expected Wide for {:?}", ar);
}
@@ -199,7 +199,7 @@ mod tests {
CommonAspectRatio::Tall,
];
for ar in tall_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.image_size, Some(GS::Tall)), "expected Tall for {:?}", ar);
}
@@ -223,7 +223,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1p5_image_request()
@@ -245,7 +245,7 @@ mod tests {
(4, GN::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -256,7 +256,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_gpt_image_1p5_image_request()
@@ -274,7 +274,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1p5_image_request()
@@ -292,7 +292,7 @@ mod tests {
#[test]
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_gpt_image_1p5_image_request()
};
@@ -304,7 +304,7 @@ mod tests {
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_gpt_image_1p5_image_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(plan.image_inputs.is_none());
}
@@ -312,7 +312,7 @@ mod tests {
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_gpt_image_1p5_image_request()
};
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
@@ -49,7 +49,7 @@ pub struct PlanArtcraftGptImage2 {
}
pub fn plan_generate_image_artcraft_gpt_image_2(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -199,7 +199,7 @@ mod tests {
#[test]
fn no_image_inputs_yields_none() {
let plan = build_plan(&GenerateImageRequest { image_inputs: None, ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { image_inputs: None, ..base_request() });
assert!(plan.image_inputs.is_none());
}
@@ -209,7 +209,7 @@ mod tests {
MediaFileToken::new_from_str("mf_test_a"),
MediaFileToken::new_from_str("mf_test_b"),
];
let plan = build_plan(&GenerateImageRequest {
let plan = build_plan(&GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_request()
});
@@ -220,7 +220,7 @@ mod tests {
#[test]
fn url_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/a.jpg".to_string()];
let plan = build_plan(&GenerateImageRequest {
let plan = build_plan(&GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls)),
..base_request()
});
@@ -241,19 +241,19 @@ mod tests {
#[test]
fn low() {
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Low), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { quality: Some(CommonQuality::Low), ..base_request() });
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::Low));
}
#[test]
fn medium() {
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Medium), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { quality: Some(CommonQuality::Medium), ..base_request() });
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::Medium));
}
#[test]
fn high() {
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::High), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { quality: Some(CommonQuality::High), ..base_request() });
assert!(matches!(plan.quality, ArtcraftGptImage2Quality::High));
}
}
@@ -265,34 +265,34 @@ mod tests {
#[test]
fn none_yields_auto() {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: None, ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: None, ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Auto)));
}
#[test]
fn auto_variants_yield_auto() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Auto)), "expected Auto for {:?}", ar);
}
}
#[test]
fn square() {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(CommonAspectRatio::Square), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Square)));
}
#[test]
fn square_hd() {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::SquareHd), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(CommonAspectRatio::SquareHd), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::SquareHd)));
}
#[test]
fn landscape_4x3_variants() {
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::WideFiveByFour] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Landscape4x3)), "expected Landscape4x3 for {:?}", ar);
}
}
@@ -305,7 +305,7 @@ mod tests {
CommonAspectRatio::WideTwentyOneByNine,
CommonAspectRatio::Wide,
] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Landscape16x9)), "expected Landscape16x9 for {:?}", ar);
}
}
@@ -313,7 +313,7 @@ mod tests {
#[test]
fn portrait_4x3_variants() {
for ar in [CommonAspectRatio::TallThreeByFour, CommonAspectRatio::TallFourByFive] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Portrait4x3)), "expected Portrait4x3 for {:?}", ar);
}
}
@@ -326,7 +326,7 @@ mod tests {
CommonAspectRatio::TallNineByTwentyOne,
CommonAspectRatio::Tall,
] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(ArtcraftGptImage2ImageSize::Portrait16x9)), "expected Portrait16x9 for {:?}", ar);
}
}
@@ -339,14 +339,14 @@ mod tests {
#[test]
fn default_is_one() {
let plan = build_plan(&GenerateImageRequest { image_batch_count: None, ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { image_batch_count: None, ..base_request() });
assert!(matches!(plan.num_images, ArtcraftGptImage2NumImages::One));
}
#[test]
fn direct_mapping() {
for (count, expected) in [(1, 1), (2, 2), (3, 3), (4, 4)] {
let plan = build_plan(&GenerateImageRequest { image_batch_count: Some(count), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_request() });
assert_eq!(plan.num_images.as_u64(), expected);
}
}
@@ -358,7 +358,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let result = plan_generate_image_artcraft_gpt_image_2(&GenerateImageRequest {
let result = plan_generate_image_artcraft_gpt_image_2(&GenerateImageRequestBuilder {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_request()
@@ -369,7 +369,7 @@ mod tests {
#[test]
fn out_of_range_error_out() {
let result = plan_generate_image_artcraft_gpt_image_2(&GenerateImageRequest {
let result = plan_generate_image_artcraft_gpt_image_2(&GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_request()
@@ -383,7 +383,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let plan = build_plan(&GenerateImageRequest {
let plan = build_plan(&GenerateImageRequestBuilder {
image_batch_count: Some(9),
request_mismatch_mitigation_strategy: strategy,
..base_request()
@@ -405,8 +405,8 @@ mod tests {
// ── Helpers ──
fn base_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage2,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -424,7 +424,7 @@ mod tests {
}
}
fn build_plan(request: &GenerateImageRequest) -> PlanArtcraftGptImage2 {
fn build_plan(request: &GenerateImageRequestBuilder) -> PlanArtcraftGptImage2 {
let ImageGenerationPlan::ArtcraftGptImage2(plan) =
plan_generate_image_artcraft_gpt_image_2(request).expect("plan should succeed")
else {
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages,
@@ -21,7 +21,7 @@ pub struct PlanArtcraftNanaBanana {
}
pub fn plan_generate_image_artcraft_nano_banana(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -163,7 +163,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_nano_banana_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(plan.aspect_ratio.is_none());
}
@@ -171,7 +171,7 @@ mod tests {
#[test]
fn aspect_ratio_direct_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_nano_banana_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(matches!(plan.aspect_ratio, Some(NbAr::OneByOne)), "expected OneByOne for {:?}", ar);
}
@@ -188,7 +188,7 @@ mod tests {
(CommonAspectRatio::WideTwentyOneByNine, NbAr::TwentyOneByNine),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_nano_banana_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -207,7 +207,7 @@ mod tests {
(CommonAspectRatio::Tall, NbAr::NineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_nano_banana_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -220,7 +220,7 @@ mod tests {
fn aspect_ratio_auto_in_edit_mode_yields_auto() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_nano_banana_image_request()
@@ -236,7 +236,7 @@ mod tests {
#[test]
fn aspect_ratio_auto_in_text_to_image_mode_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_nano_banana_image_request()
@@ -252,7 +252,7 @@ mod tests {
#[test]
fn aspect_ratio_unsupported_error_out() {
// TallNineByTwentyOne has no direct equivalent
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_nano_banana_image_request()
@@ -271,7 +271,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: strategy,
..base_nano_banana_image_request()
@@ -293,7 +293,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -316,7 +316,7 @@ mod tests {
(4, NbN::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_nano_banana_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -327,7 +327,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -346,7 +346,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -368,7 +368,7 @@ mod tests {
// cost estimation against Artcraft. URLs are accepted (and dropped) since
// cost only depends on num_images + is_edit_mode.
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_nano_banana_image_request()
};
@@ -382,7 +382,7 @@ mod tests {
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_nano_banana_image_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(plan.image_inputs.is_none());
}
@@ -390,7 +390,7 @@ mod tests {
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_nano_banana_image_request()
};
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
NanaBanana2MultiFunctionImageGenAspectRatio, NanaBanana2MultiFunctionImageGenImageResolution,
@@ -24,7 +24,7 @@ pub struct PlanArtcraftNanaBanana2 {
}
pub fn plan_generate_image_artcraft_nano_banana_2(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -191,7 +191,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_nano_banana_2_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(plan.aspect_ratio.is_none());
}
@@ -199,7 +199,7 @@ mod tests {
#[test]
fn aspect_ratio_direct_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_nano_banana_2_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(matches!(plan.aspect_ratio, Some(Nb2Ar::OneByOne)), "expected OneByOne for {:?}", ar);
}
@@ -209,7 +209,7 @@ mod tests {
fn aspect_ratio_auto_in_edit_mode_yields_auto() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_nano_banana_2_image_request()
@@ -225,7 +225,7 @@ mod tests {
#[test]
fn aspect_ratio_auto_in_text_to_image_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_nano_banana_2_image_request()
@@ -242,7 +242,7 @@ mod tests {
#[test]
fn resolution_none_is_none() {
let request = GenerateImageRequest { resolution: None, ..base_nano_banana_2_image_request() };
let request = GenerateImageRequestBuilder { resolution: None, ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(plan.resolution.is_none());
}
@@ -255,7 +255,7 @@ mod tests {
(CommonResolution::FourK, Nb2Res::FourK),
];
for (common, expected) in cases {
let request = GenerateImageRequest { resolution: Some(common), ..base_nano_banana_2_image_request() };
let request = GenerateImageRequestBuilder { resolution: Some(common), ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
@@ -266,7 +266,7 @@ mod tests {
#[test]
fn resolution_three_k_falls_back_to_two_k() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::ThreeK),
..base_nano_banana_2_image_request()
};
@@ -285,7 +285,7 @@ mod tests {
(4, Nb2N::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_nano_banana_2_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution,
@@ -24,7 +24,7 @@ pub struct PlanArtcraftNanaBananaPro {
}
pub fn plan_generate_image_artcraft_nano_banana_pro(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -189,7 +189,7 @@ mod tests {
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(plan.aspect_ratio.is_none());
}
@@ -197,7 +197,7 @@ mod tests {
#[test]
fn aspect_ratio_direct_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(matches!(plan.aspect_ratio, Some(NbpAr::OneByOne)), "expected OneByOne for {:?}", ar);
}
@@ -214,7 +214,7 @@ mod tests {
(CommonAspectRatio::WideTwentyOneByNine, NbpAr::TwentyOneByNine),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -233,7 +233,7 @@ mod tests {
(CommonAspectRatio::Tall, NbpAr::NineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -247,7 +247,7 @@ mod tests {
// Auto is only valid for edit mode (image_inputs present)
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_image_request()
@@ -263,7 +263,7 @@ mod tests {
#[test]
fn aspect_ratio_auto_in_text_to_image_mode_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_image_request()
@@ -279,7 +279,7 @@ mod tests {
#[test]
fn aspect_ratio_unsupported_error_out() {
// TallNineByTwentyOne has no direct equivalent
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_image_request()
@@ -298,7 +298,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: strategy,
..base_image_request()
@@ -315,7 +315,7 @@ mod tests {
#[test]
fn resolution_none_is_none() {
let request = GenerateImageRequest { resolution: None, ..base_image_request() };
let request = GenerateImageRequestBuilder { resolution: None, ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(plan.resolution.is_none());
}
@@ -328,7 +328,7 @@ mod tests {
(CommonResolution::FourK, NbpRes::FourK),
];
for (common, expected) in cases {
let request = GenerateImageRequest { resolution: Some(common), ..base_image_request() };
let request = GenerateImageRequestBuilder { resolution: Some(common), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
@@ -339,7 +339,7 @@ mod tests {
#[test]
fn resolution_three_k_falls_back_to_two_k() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::ThreeK),
..base_image_request()
};
@@ -356,7 +356,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -379,7 +379,7 @@ mod tests {
(4, NbpN::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -390,7 +390,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -409,7 +409,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -431,7 +431,7 @@ mod tests {
// cost estimation against Artcraft. URLs are accepted (and dropped) since
// cost only depends on num_images + resolution + is_edit_mode.
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_image_request()
};
@@ -443,7 +443,7 @@ mod tests {
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_image_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(plan.image_inputs.is_none());
}
@@ -451,7 +451,7 @@ mod tests {
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_image_request()
};
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::{
QwenEdit2511EditImageAngleImageSize,
@@ -24,7 +24,7 @@ pub struct PlanArtcraftQwenEdit2511Angles {
}
pub fn plan_generate_image_artcraft_qwen_edit_2511_angles(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -155,7 +155,7 @@ mod tests {
#[test]
fn requires_image_input() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: None,
..base_qwen_edit_2511_angles_image_request()
};
@@ -166,7 +166,7 @@ mod tests {
#[test]
fn rejects_urls() {
let urls = vec!["https://example.com/img.png".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_qwen_edit_2511_angles_image_request()
};
@@ -177,7 +177,7 @@ mod tests {
#[test]
fn accepts_single_media_token() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_qwen_edit_2511_angles_image_request()
};
@@ -188,7 +188,7 @@ mod tests {
#[test]
fn rejects_multiple_media_tokens() {
let tokens = vec![make_token(), make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_qwen_edit_2511_angles_image_request()
};
@@ -199,7 +199,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: None,
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_qwen_edit_2511_angles_image_request()
@@ -211,7 +211,7 @@ mod tests {
#[test]
fn image_size_square_maps_directly() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_qwen_edit_2511_angles_image_request()
@@ -225,7 +225,7 @@ mod tests {
let tokens = vec![make_token()];
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
@@ -242,7 +242,7 @@ mod tests {
#[test]
fn angle_fields_are_passed_through() {
let tokens = vec![make_token()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
horizontal_angle: Some(45.0),
vertical_angle: Some(-30.0),
zoom: Some(1.5),
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
BytedanceSeedreamV4MultiFunctionImageGenImageSize,
@@ -22,7 +22,7 @@ pub struct PlanArtcraftSeedream4 {
}
pub fn plan_generate_image_artcraft_seedream_4(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -159,7 +159,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_size.is_none());
}
@@ -167,7 +167,7 @@ mod tests {
#[test]
fn image_size_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
let expected = match ar {
CommonAspectRatio::Square => S::Square,
@@ -189,7 +189,7 @@ mod tests {
(CommonAspectRatio::WideFourByThree, S::LandscapeFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
@@ -206,7 +206,7 @@ mod tests {
(CommonAspectRatio::TallThreeByFour, S::PortraitFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
@@ -218,7 +218,7 @@ mod tests {
#[test]
fn image_size_auto_in_edit_mode_yields_auto() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_seedream_4_image_request()
@@ -229,7 +229,7 @@ mod tests {
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_4_image_request()
@@ -241,7 +241,7 @@ mod tests {
#[test]
fn image_size_auto2k_and_auto4k() {
for (ar, expected) in [(CommonAspectRatio::Auto2k, S::Auto2k), (CommonAspectRatio::Auto4k, S::Auto4k)] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
@@ -253,7 +253,7 @@ mod tests {
#[test]
fn image_size_unsupported_error_out() {
for ar in [CommonAspectRatio::WideFiveByFour, CommonAspectRatio::TallFourByFive] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4_image_request()
@@ -268,7 +268,7 @@ mod tests {
#[test]
fn image_size_unsupported_nearest_match() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideFiveByFour),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
@@ -276,7 +276,7 @@ mod tests {
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::LandscapeSixteenNine)), "expected LandscapeSixteenNine with {:?}", strategy);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
@@ -295,7 +295,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -312,7 +312,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -323,7 +323,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -338,7 +338,7 @@ mod tests {
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -354,7 +354,7 @@ mod tests {
#[test]
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_seedream_4_image_request()
};
@@ -366,7 +366,7 @@ mod tests {
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_seedream_4_image_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_inputs.is_none());
}
@@ -374,7 +374,7 @@ mod tests {
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_seedream_4_image_request()
};
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize,
@@ -22,7 +22,7 @@ pub struct PlanArtcraftSeedream4p5 {
}
pub fn plan_generate_image_artcraft_seedream_4p5(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -159,7 +159,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_size.is_none());
}
@@ -167,7 +167,7 @@ mod tests {
#[test]
fn image_size_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
let expected = match ar {
CommonAspectRatio::Square => S::Square,
@@ -189,7 +189,7 @@ mod tests {
(CommonAspectRatio::WideFourByThree, S::LandscapeFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
@@ -206,7 +206,7 @@ mod tests {
(CommonAspectRatio::TallThreeByFour, S::PortraitFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
@@ -219,7 +219,7 @@ mod tests {
fn image_size_auto_in_edit_mode_yields_auto2k() {
// V4.5 has no bare Auto; falls back to Auto2k in edit mode
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_seedream_4p5_image_request()
@@ -230,7 +230,7 @@ mod tests {
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_4p5_image_request()
@@ -242,7 +242,7 @@ mod tests {
#[test]
fn image_size_auto2k_and_auto4k() {
for (ar, expected) in [(CommonAspectRatio::Auto2k, S::Auto2k), (CommonAspectRatio::Auto4k, S::Auto4k)] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
@@ -254,7 +254,7 @@ mod tests {
#[test]
fn image_size_unsupported_error_out() {
for ar in [CommonAspectRatio::WideFiveByFour, CommonAspectRatio::TallFourByFive] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4p5_image_request()
@@ -269,7 +269,7 @@ mod tests {
#[test]
fn image_size_unsupported_nearest_match() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideFiveByFour),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
@@ -277,7 +277,7 @@ mod tests {
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::LandscapeSixteenNine)), "expected LandscapeSixteenNine with {:?}", strategy);
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
@@ -296,7 +296,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -313,7 +313,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -324,7 +324,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -339,7 +339,7 @@ mod tests {
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -355,7 +355,7 @@ mod tests {
#[test]
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_seedream_4p5_image_request()
};
@@ -367,7 +367,7 @@ mod tests {
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_seedream_4p5_image_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_inputs.is_none());
}
@@ -375,7 +375,7 @@ mod tests {
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_seedream_4p5_image_request()
};
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
BytedanceSeedream5LiteMultiFunctionImageGenImageSize,
@@ -22,7 +22,7 @@ pub struct PlanArtcraftSeedream5Lite {
}
pub fn plan_generate_image_artcraft_seedream_5_lite(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -157,7 +157,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_5_lite_image_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_seedream_5_lite_image_request() };
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(plan.image_size.is_none());
}
@@ -165,7 +165,7 @@ mod tests {
#[test]
fn image_size_auto_in_edit_mode_yields_auto2k() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_seedream_5_lite_image_request()
@@ -176,7 +176,7 @@ mod tests {
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_5_lite_image_request()
@@ -188,7 +188,7 @@ mod tests {
#[test]
fn auto4k_is_mapped_to_auto3k() {
// 5 Lite only supports up to auto_3K; auto_4K is downgraded
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto4k),
..base_seedream_5_lite_image_request()
};
@@ -200,7 +200,7 @@ mod tests {
fn num_images_direct_mapping() {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_5_lite_image_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_seedream_5_lite_image_request() };
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::Flux1DevEditImageNumImages;
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
@@ -29,7 +29,7 @@ pub struct PlanFalFlux1Dev {
}
pub fn plan_generate_image_fal_flux_1_dev(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let maybe_image_url = resolve_single_image_url(request.image_inputs.clone())?;
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageSize,
@@ -32,7 +32,7 @@ pub struct PlanFalFlux1Schnell {
}
pub fn plan_generate_image_fal_flux_1_schnell(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let maybe_image_url = resolve_single_image_url(request.image_inputs.clone())?;
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
FluxPro11AspectRatio, FluxPro11NumImages,
@@ -24,7 +24,7 @@ pub struct PlanFalFluxPro11 {
}
pub fn plan_generate_image_fal_flux_pro_1p1(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -2,7 +2,7 @@ use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages,
@@ -24,7 +24,7 @@ pub struct PlanFalFluxPro11Ultra {
}
pub fn plan_generate_image_fal_flux_pro_1p1_ultra(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{
EnqueueGptImage1EditImageNumImages, EnqueueGptImage1EditImageQuality,
@@ -52,7 +52,7 @@ pub struct PlanFalGptImage1 {
}
pub fn plan_generate_image_fal_gpt_image_1(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs.clone())?;
@@ -219,8 +219,8 @@ mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
fn base_fal_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_fal_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
@@ -238,7 +238,7 @@ mod tests {
}
}
fn build_plan(request: &GenerateImageRequest) -> PlanFalGptImage1 {
fn build_plan(request: &GenerateImageRequestBuilder) -> PlanFalGptImage1 {
let ImageGenerationPlan::FalGptImage1(plan) =
plan_generate_image_fal_gpt_image_1(request).expect("plan should succeed")
else {
@@ -251,7 +251,7 @@ mod tests {
#[test]
fn no_image_inputs_yields_empty_urls() {
let request = GenerateImageRequest { image_inputs: None, ..base_fal_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_urls.is_empty());
}
@@ -262,7 +262,7 @@ mod tests {
"https://example.com/a.jpg".to_string(),
"https://example.com/b.jpg".to_string(),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_fal_request()
};
@@ -273,7 +273,7 @@ mod tests {
#[test]
fn media_token_inputs_return_error() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_fal_request()
};
@@ -297,7 +297,7 @@ mod tests {
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_size.is_none());
}
@@ -305,7 +305,7 @@ mod tests {
#[test]
fn image_size_auto_variants_yield_none() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(auto_ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar);
}
@@ -314,7 +314,7 @@ mod tests {
#[test]
fn image_size_square_variants() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square)), "expected Square for {:?}", ar);
}
@@ -331,7 +331,7 @@ mod tests {
CommonAspectRatio::Wide,
];
for ar in wide_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal)), "expected Horizontal for {:?}", ar);
}
@@ -348,7 +348,7 @@ mod tests {
CommonAspectRatio::Tall,
];
for ar in tall_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical)), "expected Vertical for {:?}", ar);
}
@@ -363,7 +363,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
@@ -378,7 +378,7 @@ mod tests {
#[test]
fn num_images_default_is_one() {
let request = GenerateImageRequest { image_batch_count: None, ..base_fal_request() };
let request = GenerateImageRequestBuilder { image_batch_count: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.num_images, FalGptImage1NumImages::One));
}
@@ -392,7 +392,7 @@ mod tests {
(4, FalGptImage1NumImages::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_fal_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_fal_request() };
let plan = build_plan(&request);
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -403,7 +403,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_fal_request()
@@ -421,7 +421,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_batch_count: Some(9),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageQuality,
@@ -49,7 +49,7 @@ pub struct PlanFalGptImage1p5 {
}
pub fn plan_generate_image_fal_gpt_image_1p5(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs.clone())?;
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{
EnqueueGptImage2EditImageNumImages, EnqueueGptImage2EditImageQuality,
@@ -53,7 +53,7 @@ pub struct PlanFalGptImage2 {
}
pub fn plan_generate_image_fal_gpt_image_2(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs.clone())?;
@@ -236,7 +236,7 @@ mod tests {
#[test]
fn no_image_inputs_yields_empty_urls() {
let plan = build_plan(&GenerateImageRequest { image_inputs: None, ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { image_inputs: None, ..base_request() });
assert!(plan.image_urls.is_empty());
}
@@ -246,7 +246,7 @@ mod tests {
"https://example.com/a.jpg".to_string(),
"https://example.com/b.jpg".to_string(),
];
let plan = build_plan(&GenerateImageRequest {
let plan = build_plan(&GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_request()
});
@@ -255,7 +255,7 @@ mod tests {
#[test]
fn media_token_inputs_return_error() {
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequest {
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
..base_request()
});
@@ -276,19 +276,19 @@ mod tests {
#[test]
fn low() {
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Low), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { quality: Some(CommonQuality::Low), ..base_request() });
assert!(matches!(plan.quality, FalGptImage2Quality::Low));
}
#[test]
fn medium() {
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::Medium), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { quality: Some(CommonQuality::Medium), ..base_request() });
assert!(matches!(plan.quality, FalGptImage2Quality::Medium));
}
#[test]
fn high() {
let plan = build_plan(&GenerateImageRequest { quality: Some(CommonQuality::High), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { quality: Some(CommonQuality::High), ..base_request() });
assert!(matches!(plan.quality, FalGptImage2Quality::High));
}
}
@@ -300,34 +300,34 @@ mod tests {
#[test]
fn none_yields_auto() {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: None, ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: None, ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Auto)));
}
#[test]
fn auto_variants_yield_auto() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Auto)), "expected Auto for {:?}", ar);
}
}
#[test]
fn square() {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(CommonAspectRatio::Square), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Square)));
}
#[test]
fn square_hd() {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::SquareHd), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(CommonAspectRatio::SquareHd), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::SquareHd)));
}
#[test]
fn landscape_4x3_variants() {
for ar in [CommonAspectRatio::WideFourByThree, CommonAspectRatio::WideFiveByFour] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Landscape4x3)), "expected Landscape4x3 for {:?}", ar);
}
}
@@ -340,7 +340,7 @@ mod tests {
CommonAspectRatio::WideTwentyOneByNine,
CommonAspectRatio::Wide,
] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Landscape16x9)), "expected Landscape16x9 for {:?}", ar);
}
}
@@ -348,7 +348,7 @@ mod tests {
#[test]
fn portrait_4x3_variants() {
for ar in [CommonAspectRatio::TallThreeByFour, CommonAspectRatio::TallFourByFive] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Portrait4x3)), "expected Portrait4x3 for {:?}", ar);
}
}
@@ -361,7 +361,7 @@ mod tests {
CommonAspectRatio::TallNineByTwentyOne,
CommonAspectRatio::Tall,
] {
let plan = build_plan(&GenerateImageRequest { aspect_ratio: Some(ar), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_request() });
assert!(matches!(plan.image_size, Some(FalGptImage2ImageSize::Portrait16x9)), "expected Portrait16x9 for {:?}", ar);
}
}
@@ -374,14 +374,14 @@ mod tests {
#[test]
fn default_is_one() {
let plan = build_plan(&GenerateImageRequest { image_batch_count: None, ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { image_batch_count: None, ..base_request() });
assert!(matches!(plan.num_images, FalGptImage2NumImages::One));
}
#[test]
fn direct_mapping() {
for (count, expected) in [(1, 1), (2, 2), (3, 3), (4, 4)] {
let plan = build_plan(&GenerateImageRequest { image_batch_count: Some(count), ..base_request() });
let plan = build_plan(&GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_request() });
assert_eq!(plan.num_images.as_u64(), expected);
}
}
@@ -393,7 +393,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequest {
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequestBuilder {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_request()
@@ -404,7 +404,7 @@ mod tests {
#[test]
fn out_of_range_error_out() {
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequest {
let result = plan_generate_image_fal_gpt_image_2(&GenerateImageRequestBuilder {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_request()
@@ -418,7 +418,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let plan = build_plan(&GenerateImageRequest {
let plan = build_plan(&GenerateImageRequestBuilder {
image_batch_count: Some(9),
request_mismatch_mitigation_strategy: strategy,
..base_request()
@@ -505,8 +505,8 @@ mod tests {
// ── Helpers ──
fn base_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage2,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
@@ -524,7 +524,7 @@ mod tests {
}
}
fn build_plan(request: &GenerateImageRequest) -> PlanFalGptImage2 {
fn build_plan(request: &GenerateImageRequestBuilder) -> PlanFalGptImage2 {
let ImageGenerationPlan::FalGptImage2(plan) =
plan_generate_image_fal_gpt_image_2(request).expect("plan should succeed")
else {
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::Gemini25FlashEditAspectRatio;
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::Gemini25FlashTextToImageAspectRatio;
@@ -31,7 +31,7 @@ pub struct PlanFalNanoBanana {
}
pub fn plan_generate_image_fal_nano_banana(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::EnqueueNanoBanana2EditImageAspectRatio;
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::EnqueueNanoBanana2TextToImageAspectRatio;
@@ -43,7 +43,7 @@ pub struct PlanFalNanaBanana2 {
}
pub fn plan_generate_image_fal_nano_banana_2(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::EnqueueNanoBananaProEditImageAspectRatio;
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::EnqueueNanoBananaProTextToImageAspectRatio;
@@ -44,7 +44,7 @@ pub struct PlanFalNanaBananaPro {
}
pub fn plan_generate_image_fal_nano_banana_pro(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -244,12 +244,12 @@ mod tests {
use crate::api::provider::Provider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::EnqueueNanoBananaProEditImageAspectRatio as EditAr;
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::EnqueueNanoBananaProTextToImageAspectRatio as T2iAr;
fn base_fal_request() -> GenerateImageRequest {
GenerateImageRequest {
fn base_fal_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
@@ -268,7 +268,7 @@ mod tests {
}
fn build_plan(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> PlanFalNanaBananaPro {
let ImageGenerationPlan::FalNanaBananaPro(plan) = plan_generate_image_fal_nano_banana_pro(request).expect("plan should succeed") else {
panic!("expected FalNanaBananaPro variant")
@@ -280,7 +280,7 @@ mod tests {
#[test]
fn no_image_inputs_yields_empty_urls() {
let request = GenerateImageRequest { image_inputs: None, ..base_fal_request() };
let request = GenerateImageRequestBuilder { image_inputs: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_urls.is_empty());
}
@@ -291,7 +291,7 @@ mod tests {
"https://example.com/a.jpg".to_string(),
"https://example.com/b.jpg".to_string(),
];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_fal_request()
};
@@ -302,7 +302,7 @@ mod tests {
#[test]
fn media_token_inputs_return_error() {
let tokens = vec![];
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_fal_request()
};
@@ -317,7 +317,7 @@ mod tests {
#[test]
fn t2i_aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.t2i_aspect_ratio.is_none());
}
@@ -325,7 +325,7 @@ mod tests {
#[test]
fn t2i_aspect_ratio_auto_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_fal_request()
@@ -356,7 +356,7 @@ mod tests {
(CommonAspectRatio::Tall, T2iAr::NineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_fal_request() };
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_fal_request() };
let plan = build_plan(&request);
assert!(
matches!(plan.t2i_aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
@@ -367,7 +367,7 @@ mod tests {
#[test]
fn t2i_aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_fal_request()
@@ -385,7 +385,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
@@ -404,7 +404,7 @@ mod tests {
fn edit_aspect_ratio_auto_in_edit_mode_yields_auto() {
let urls = vec!["https://example.com/img.jpg".to_string()];
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_fal_request()
@@ -420,7 +420,7 @@ mod tests {
#[test]
fn edit_aspect_ratio_auto_in_text_to_image_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_fal_request()
@@ -437,7 +437,7 @@ mod tests {
#[test]
fn resolution_none_is_none() {
let request = GenerateImageRequest { resolution: None, ..base_fal_request() };
let request = GenerateImageRequestBuilder { resolution: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.resolution.is_none());
}
@@ -450,7 +450,7 @@ mod tests {
(CommonResolution::FourK, FalNbpResolution::FourK),
];
for (common, expected) in cases {
let request = GenerateImageRequest { resolution: Some(common), ..base_fal_request() };
let request = GenerateImageRequestBuilder { resolution: Some(common), ..base_fal_request() };
let plan = build_plan(&request);
assert!(
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
@@ -461,7 +461,7 @@ mod tests {
#[test]
fn resolution_three_k_falls_back_to_two_k() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::ThreeK),
..base_fal_request()
};
@@ -478,7 +478,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
@@ -501,7 +501,7 @@ mod tests {
(4, FalNbpNumImages::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_fal_request() };
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_fal_request() };
let plan = build_plan(&request);
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
@@ -512,7 +512,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
@@ -531,7 +531,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{
EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize,
@@ -44,7 +44,7 @@ pub struct PlanFalSeedream4 {
}
pub fn plan_generate_image_fal_seedream_4(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
@@ -3,7 +3,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{
EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize,
@@ -42,7 +42,7 @@ pub struct PlanFalSeedream4p5 {
}
pub fn plan_generate_image_fal_seedream_4p5(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs.clone())?;
@@ -4,7 +4,7 @@ use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{
EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize,
@@ -43,7 +43,7 @@ pub struct PlanFalSeedream5Lite {
}
pub fn plan_generate_image_fal_seedream_5_lite(
request: &GenerateImageRequest,
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs.clone())?;
@@ -0,0 +1,28 @@
use crate::api::provider::Provider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::image_generation_draft_context::ImageGenerationDraftContext;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
/// Wrapper for all image generation draft requests.
///
/// Drafts hold validated/planned parameters but may still need media resolution
/// (e.g. uploading local files to a provider) before they can be sent.
/// Currently empty — FAL doesn't need a draft phase.
#[derive(Clone, Debug)]
pub enum ImageGenerationDraftRequest {
}
impl ImageGenerationDraftRequest {
pub fn get_provider(&self) -> Provider {
match *self {}
}
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
match *self {}
}
pub async fn finalize(self, _draft_context: ImageGenerationDraftContext<'_>) -> Result<ImageGenerationRequest, ArtcraftRouterError> {
match self {}
}
}
@@ -0,0 +1,18 @@
use std::fmt::Debug;
/// Context passed when finalizing a draft into a concrete request.
///
/// Currently empty — FAL doesn't need a draft phase so no context is required.
/// Future providers that need media uploads during finalization will add fields here
/// (e.g. a router client reference, media-file-to-URL maps, etc.).
#[derive(Clone, Default)]
pub struct ImageGenerationDraftContext<'a> {
/// Prevent construction without Default; reserving the lifetime for future use.
_phantom: std::marker::PhantomData<&'a ()>,
}
impl Debug for ImageGenerationDraftContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageGenerationDraftContext").finish()
}
}
@@ -0,0 +1,27 @@
use crate::api::provider::Provider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
#[derive(Debug, Clone)]
pub enum ImageGenerationDraftOrRequest {
Draft(ImageGenerationDraftRequest),
Request(ImageGenerationRequest),
}
impl ImageGenerationDraftOrRequest {
pub fn get_provider(&self) -> Provider {
match self {
Self::Draft(draft) => draft.get_provider(),
Self::Request(request) => request.get_provider(),
}
}
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
match self {
Self::Draft(draft) => draft.estimate_cost(),
Self::Request(request) => request.estimate_cost(),
}
}
}
@@ -0,0 +1,70 @@
use crate::api::provider::Provider;
use crate::client::router_client::RouterClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::cost::FalFlux1DevCostState;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
#[derive(Clone, Debug)]
pub enum ImageGenerationRequest {
FalFlux1Dev(FalFlux1DevRequestState),
FalFlux1Schnell(FalFlux1SchnellRequestState),
FalNanoBanana2(FalNanoBanana2RequestState),
FalNanoBananaPro(FalNanoBananaProRequestState),
}
impl ImageGenerationRequest {
pub fn get_provider(&self) -> Provider {
match self {
Self::FalFlux1Dev(_) => Provider::Fal,
Self::FalFlux1Schnell(_) => Provider::Fal,
Self::FalNanoBanana2(_) => Provider::Fal,
Self::FalNanoBananaPro(_) => Provider::Fal,
}
}
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
match self {
Self::FalFlux1Dev(request) => {
Ok(FalFlux1DevCostState::from_request(request).estimate_cost())
}
Self::FalFlux1Schnell(request) => {
Ok(FalFlux1SchnellCostState::from_request(request).estimate_cost())
}
Self::FalNanoBanana2(request) => {
Ok(FalNanoBanana2CostState::from_request(request).estimate_cost())
}
Self::FalNanoBananaPro(request) => {
Ok(FalNanoBananaProCostState::from_request(request).estimate_cost())
}
}
}
pub async fn send_request(&self, client: &RouterClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
Self::FalFlux1Dev(request) => {
let fal_client = client.get_fal_webhook_optional_client_ref()?;
request.send(fal_client).await
}
Self::FalFlux1Schnell(request) => {
let fal_client = client.get_fal_webhook_optional_client_ref()?;
request.send(fal_client).await
}
Self::FalNanoBanana2(request) => {
let fal_client = client.get_fal_webhook_optional_client_ref()?;
request.send(fal_client).await
}
Self::FalNanoBananaPro(request) => {
let fal_client = client.get_fal_webhook_optional_client_ref()?;
request.send(fal_client).await
}
}
}
}
@@ -0,0 +1,5 @@
pub mod image_generation_draft;
pub mod image_generation_draft_context;
pub mod image_generation_draft_or_request;
pub mod image_generation_request;
pub mod providers;
@@ -0,0 +1,428 @@
use fal_client::requests::api::image::edit::flux_1_dev_edit_image::api::{
Flux1DevEditImageNumImages, Flux1DevEditImageRequest,
};
use fal_client::requests::api::image::text::flux_1_dev_text_to_image::api::{
Flux1DevTextToImageAspectRatio, Flux1DevTextToImageNumImages, Flux1DevTextToImageRequest,
};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
pub fn build_fal_flux_1_dev(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let prompt = builder.prompt.clone().unwrap_or_default();
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
let image_url = resolve_single_image_url(builder.image_inputs.clone())?;
let state = if let Some(url) = image_url {
// Edit image (single image URL, no aspect ratio / resolution controls)
FalFlux1DevRequestState::EditImage(Flux1DevEditImageRequest {
prompt,
image_url: url,
num_images: to_edit_num_images(num_images),
})
} else {
// Text-to-image
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio);
FalFlux1DevRequestState::TextToImage(Flux1DevTextToImageRequest {
prompt,
num_images: to_t2i_num_images(num_images),
aspect_ratio,
})
};
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFlux1Dev(state),
))
}
// ── Num images ──
#[derive(Copy, Clone, Debug)]
enum PlannedNumImages {
One,
Two,
Three,
Four,
}
fn plan_num_images(
count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<PlannedNumImages, ArtcraftRouterError> {
let count = count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(PlannedNumImages::One),
2 => Ok(PlannedNumImages::Two),
3 => Ok(PlannedNumImages::Three),
4 => Ok(PlannedNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(PlannedNumImages::Four),
},
}
}
fn to_t2i_num_images(n: PlannedNumImages) -> Flux1DevTextToImageNumImages {
match n {
PlannedNumImages::One => Flux1DevTextToImageNumImages::One,
PlannedNumImages::Two => Flux1DevTextToImageNumImages::Two,
PlannedNumImages::Three => Flux1DevTextToImageNumImages::Three,
PlannedNumImages::Four => Flux1DevTextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: PlannedNumImages) -> Flux1DevEditImageNumImages {
match n {
PlannedNumImages::One => Flux1DevEditImageNumImages::One,
PlannedNumImages::Two => Flux1DevEditImageNumImages::Two,
PlannedNumImages::Three => Flux1DevEditImageNumImages::Three,
PlannedNumImages::Four => Flux1DevEditImageNumImages::Four,
}
}
// ── Aspect ratio ──
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
) -> Flux1DevTextToImageAspectRatio {
use Flux1DevTextToImageAspectRatio as T;
match aspect_ratio {
None | Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => T::SquareHd,
Some(CommonAspectRatio::Square) => T::Square,
Some(CommonAspectRatio::SquareHd) => T::SquareHd,
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideFiveByFour)
| Some(CommonAspectRatio::WideThreeByTwo) | Some(CommonAspectRatio::Wide) => T::LandscapeFourByThree,
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::WideTwentyOneByNine) => T::LandscapeSixteenByNine,
Some(CommonAspectRatio::TallThreeByFour) | Some(CommonAspectRatio::TallFourByFive)
| Some(CommonAspectRatio::TallTwoByThree) | Some(CommonAspectRatio::Tall) => T::PortraitThreeByFour,
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::TallNineByTwentyOne) => T::PortraitNineBySixteen,
}
}
// ── Image inputs ──
/// Flux 1 Dev edit takes a single image URL, not a list.
/// If multiple URLs are provided, we take the first one.
fn resolve_single_image_url(
image_inputs: Option<ImageListRef>,
) -> Result<Option<String>, ArtcraftRouterError> {
match image_inputs {
None => Ok(None),
Some(ImageListRef::Urls(urls)) => {
Ok(urls.into_iter().next())
}
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Dev,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> Flux1DevTextToImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFlux1Dev(
FalFlux1DevRequestState::TextToImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected TextToImage variant")
};
req
}
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> Flux1DevEditImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFlux1Dev(
FalFlux1DevRequestState::EditImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected EditImage variant")
};
req
}
// ── Mode detection ──
mod mode_detection {
use super::*;
#[test]
fn no_images_yields_text_to_image() {
let req = unwrap_t2i(build_fal_flux_1_dev(base_builder()));
assert_eq!(req.prompt, "a cat in space");
}
#[test]
fn single_url_yields_edit_image() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_dev(builder));
assert_eq!(req.image_url, "https://example.com/img.jpg");
}
#[test]
fn multiple_urls_takes_first() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec![
"https://example.com/a.jpg".to_string(),
"https://example.com/b.jpg".to_string(),
])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_dev(builder));
assert_eq!(req.image_url, "https://example.com/a.jpg");
}
#[test]
fn empty_urls_yields_text_to_image() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec![])),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert_eq!(req.prompt, "a cat in space");
}
#[test]
fn media_tokens_return_error() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
..base_builder()
};
assert!(matches!(
build_fal_flux_1_dev(builder),
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
}
// ── Num images ──
mod num_images_tests {
use super::*;
#[test]
fn default_is_one() {
let req = unwrap_t2i(build_fal_flux_1_dev(base_builder()));
assert!(matches!(req.num_images, Flux1DevTextToImageNumImages::One));
}
#[test]
fn explicit_three() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.num_images, Flux1DevTextToImageNumImages::Three));
}
#[test]
fn zero_is_error() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(0),
..base_builder()
};
assert!(matches!(
build_fal_flux_1_dev(builder),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
#[test]
fn over_four_error_out() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
..base_builder()
};
assert!(matches!(
build_fal_flux_1_dev(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn over_four_clamps_with_upgrade() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.num_images, Flux1DevTextToImageNumImages::Four));
}
#[test]
fn edit_mode_num_images() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
image_batch_count: Some(2),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_dev(builder));
assert!(matches!(req.num_images, Flux1DevEditImageNumImages::Two));
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn default_is_square_hd() {
let req = unwrap_t2i(build_fal_flux_1_dev(base_builder()));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::SquareHd));
}
#[test]
fn auto_yields_square_hd() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::SquareHd));
}
#[test]
fn square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::Square));
}
#[test]
fn square_hd() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::SquareHd));
}
#[test]
fn wide_four_by_three() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideFourByThree),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::LandscapeFourByThree));
}
#[test]
fn wide_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::LandscapeSixteenByNine));
}
#[test]
fn wide_twenty_one_by_nine_maps_to_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::LandscapeSixteenByNine));
}
#[test]
fn tall_three_by_four() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallThreeByFour),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::PortraitThreeByFour));
}
#[test]
fn tall_nine_by_sixteen() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert!(matches!(req.aspect_ratio, Flux1DevTextToImageAspectRatio::PortraitNineBySixteen));
}
}
// ── Prompt ──
mod prompt_tests {
use super::*;
#[test]
fn prompt_is_passed_through() {
let builder = GenerateImageRequestBuilder {
prompt: Some("my custom prompt".to_string()),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert_eq!(req.prompt, "my custom prompt");
}
#[test]
fn missing_prompt_defaults_to_empty() {
let builder = GenerateImageRequestBuilder {
prompt: None,
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_dev(builder));
assert_eq!(req.prompt, "");
}
}
}
@@ -0,0 +1,111 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
pub struct FalFlux1DevCostState {
cost_in_usd_cents: u64,
}
impl FalFlux1DevCostState {
pub fn from_request(request: &FalFlux1DevRequestState) -> Self {
let cost_in_usd_cents = match request {
FalFlux1DevRequestState::TextToImage(req) => req.calculate_cost_in_cents(),
FalFlux1DevRequestState::EditImage(req) => req.calculate_cost_in_cents(),
};
Self { cost_in_usd_cents }
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
ImageGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::requests::api::image::edit::flux_1_dev_edit_image::api::{
Flux1DevEditImageNumImages, Flux1DevEditImageRequest,
};
use fal_client::requests::api::image::text::flux_1_dev_text_to_image::api::{
Flux1DevTextToImageAspectRatio, Flux1DevTextToImageNumImages, Flux1DevTextToImageRequest,
};
mod text_to_image_costs {
use super::*;
fn make_t2i(num_images: Flux1DevTextToImageNumImages) -> FalFlux1DevRequestState {
FalFlux1DevRequestState::TextToImage(Flux1DevTextToImageRequest {
prompt: "test".to_string(),
num_images,
aspect_ratio: Flux1DevTextToImageAspectRatio::Square,
})
}
#[test]
fn one_image() {
let cost = FalFlux1DevCostState::from_request(&make_t2i(Flux1DevTextToImageNumImages::One));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(3));
}
#[test]
fn two_images() {
let cost = FalFlux1DevCostState::from_request(&make_t2i(Flux1DevTextToImageNumImages::Two));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(6));
}
#[test]
fn four_images() {
let cost = FalFlux1DevCostState::from_request(&make_t2i(Flux1DevTextToImageNumImages::Four));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(12));
}
}
mod edit_image_costs {
use super::*;
fn make_edit(num_images: Flux1DevEditImageNumImages) -> FalFlux1DevRequestState {
FalFlux1DevRequestState::EditImage(Flux1DevEditImageRequest {
prompt: "test".to_string(),
image_url: "https://example.com/img.jpg".to_string(),
num_images,
})
}
#[test]
fn one_image() {
let cost = FalFlux1DevCostState::from_request(&make_edit(Flux1DevEditImageNumImages::One));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(3));
}
#[test]
fn three_images() {
let cost = FalFlux1DevCostState::from_request(&make_edit(Flux1DevEditImageNumImages::Three));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(9));
}
}
#[test]
fn cost_flags_are_correct() {
let state = FalFlux1DevCostState::from_request(
&FalFlux1DevRequestState::TextToImage(Flux1DevTextToImageRequest {
prompt: "test".to_string(),
num_images: Flux1DevTextToImageNumImages::One,
aspect_ratio: Flux1DevTextToImageAspectRatio::Square,
}),
);
let cost = state.estimate_cost();
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert!(cost.cost_in_credits.is_none());
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,176 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::api::image::edit::flux_1_dev_edit_image::api::Flux1DevEditImageRequest;
use fal_client::requests::api::image::text::flux_1_dev_text_to_image::api::Flux1DevTextToImageRequest;
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
#[derive(Clone, Debug)]
pub enum FalFlux1DevRequestState {
TextToImage(Flux1DevTextToImageRequest),
EditImage(Flux1DevEditImageRequest),
}
impl FalFlux1DevRequestState {
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
Self::TextToImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
Self::EditImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
}
}
}
// ── Helpers ──
struct FalResponseIds {
request_id: Option<String>,
gateway_request_id: Option<String>,
}
async fn send_fal_request<T: FalEndpoint>(
request: &T,
client: &RouterFalWebhookOptionalClient,
) -> Result<FalResponseIds, ArtcraftRouterError> {
if let Some(webhook_url) = &client.webhook_url {
let response = request
.send_webhook_request(&client.api_key, webhook_url)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: response.request_id,
gateway_request_id: response.gateway_request_id,
})
} else {
let response = request
.send_queue_request(&client.api_key)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: Some(response.request_id),
gateway_request_id: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::api::image::edit::flux_1_dev_edit_image::api::Flux1DevEditImageNumImages;
use fal_client::requests::api::image::text::flux_1_dev_text_to_image::api::{
Flux1DevTextToImageAspectRatio, Flux1DevTextToImageNumImages,
};
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
fn read_fal_api_key() -> FalApiKey {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read fal_api_key.txt");
FalApiKey::from_str(secret.trim())
}
fn client_with_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new_with_webhook(
read_fal_api_key(),
"https://example.com/fal-webhook-test".to_string(),
)
}
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new(read_fal_api_key())
}
// ── Text-to-image ──
mod text_to_image {
use super::*;
fn t2i_request() -> FalFlux1DevRequestState {
FalFlux1DevRequestState::TextToImage(Flux1DevTextToImageRequest {
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
num_images: Flux1DevTextToImageNumImages::One,
aspect_ratio: Flux1DevTextToImageAspectRatio::LandscapeSixteenByNine,
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let state = t2i_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let state = t2i_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue t2i — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
// ── Edit image ──
mod edit_image {
use super::*;
fn edit_request() -> FalFlux1DevRequestState {
FalFlux1DevRequestState::EditImage(Flux1DevEditImageRequest {
prompt: "turn this into a watercolor painting".to_string(),
image_url: JUNO_AT_LAKE_IMAGE_URL.to_string(),
num_images: Flux1DevEditImageNumImages::One,
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let state = edit_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook edit — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let state = edit_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue edit — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
}
@@ -0,0 +1,439 @@
use fal_client::requests::api::image::edit::flux_1_schnell_edit_image::api::{
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageRequest, Flux1SchnellEditImageSize,
};
use fal_client::requests::api::image::text::flux_1_schnell_text_to_image::api::{
Flux1SchnellTextToImageAspectRatio, Flux1SchnellTextToImageNumImages,
Flux1SchnellTextToImageRequest,
};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
pub fn build_fal_flux_1_schnell(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let prompt = builder.prompt.clone().unwrap_or_default();
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
let image_url = resolve_single_image_url(builder.image_inputs.clone())?;
let state = if let Some(url) = image_url {
// Edit image (redux): single image URL, optional image_size, no prompt
let image_size = plan_edit_image_size(builder.aspect_ratio);
FalFlux1SchnellRequestState::EditImage(Flux1SchnellEditImageRequest {
image_url: url,
num_images: to_edit_num_images(num_images),
image_size,
})
} else {
// Text-to-image
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio);
FalFlux1SchnellRequestState::TextToImage(Flux1SchnellTextToImageRequest {
prompt,
num_images: to_t2i_num_images(num_images),
aspect_ratio,
})
};
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFlux1Schnell(state),
))
}
// ── Num images ──
#[derive(Copy, Clone, Debug)]
enum PlannedNumImages {
One,
Two,
Three,
Four,
}
fn plan_num_images(
count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<PlannedNumImages, ArtcraftRouterError> {
let count = count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(PlannedNumImages::One),
2 => Ok(PlannedNumImages::Two),
3 => Ok(PlannedNumImages::Three),
4 => Ok(PlannedNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(PlannedNumImages::Four),
},
}
}
fn to_t2i_num_images(n: PlannedNumImages) -> Flux1SchnellTextToImageNumImages {
match n {
PlannedNumImages::One => Flux1SchnellTextToImageNumImages::One,
PlannedNumImages::Two => Flux1SchnellTextToImageNumImages::Two,
PlannedNumImages::Three => Flux1SchnellTextToImageNumImages::Three,
PlannedNumImages::Four => Flux1SchnellTextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: PlannedNumImages) -> Flux1SchnellEditImageNumImages {
match n {
PlannedNumImages::One => Flux1SchnellEditImageNumImages::One,
PlannedNumImages::Two => Flux1SchnellEditImageNumImages::Two,
PlannedNumImages::Three => Flux1SchnellEditImageNumImages::Three,
PlannedNumImages::Four => Flux1SchnellEditImageNumImages::Four,
}
}
// ── Aspect ratio ──
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
) -> Flux1SchnellTextToImageAspectRatio {
use Flux1SchnellTextToImageAspectRatio as T;
match aspect_ratio {
None | Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => T::SquareHd,
Some(CommonAspectRatio::Square) => T::Square,
Some(CommonAspectRatio::SquareHd) => T::SquareHd,
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideFiveByFour)
| Some(CommonAspectRatio::WideThreeByTwo) | Some(CommonAspectRatio::Wide) => T::LandscapeFourByThree,
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::WideTwentyOneByNine) => T::LandscapeSixteenByNine,
Some(CommonAspectRatio::TallThreeByFour) | Some(CommonAspectRatio::TallFourByFive)
| Some(CommonAspectRatio::TallTwoByThree) | Some(CommonAspectRatio::Tall) => T::PortraitThreeByFour,
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::TallNineByTwentyOne) => T::PortraitNineBySixteen,
}
}
fn plan_edit_image_size(
aspect_ratio: Option<CommonAspectRatio>,
) -> Option<Flux1SchnellEditImageSize> {
use Flux1SchnellEditImageSize as S;
aspect_ratio.and_then(|ar| match ar {
CommonAspectRatio::Auto | CommonAspectRatio::Auto2k
| CommonAspectRatio::Auto3k | CommonAspectRatio::Auto4k => None,
CommonAspectRatio::Square => Some(S::Square),
CommonAspectRatio::SquareHd => Some(S::SquareHd),
CommonAspectRatio::WideFourByThree | CommonAspectRatio::WideFiveByFour
| CommonAspectRatio::WideThreeByTwo | CommonAspectRatio::Wide => Some(S::LandscapeFourByThree),
CommonAspectRatio::WideSixteenByNine | CommonAspectRatio::WideTwentyOneByNine => Some(S::LandscapeSixteenByNine),
CommonAspectRatio::TallThreeByFour | CommonAspectRatio::TallFourByFive
| CommonAspectRatio::TallTwoByThree | CommonAspectRatio::Tall => Some(S::PortraitThreeByFour),
CommonAspectRatio::TallNineBySixteen | CommonAspectRatio::TallNineByTwentyOne => Some(S::PortraitNineBySixteen),
})
}
// ── Image inputs ──
fn resolve_single_image_url(
image_inputs: Option<ImageListRef>,
) -> Result<Option<String>, ArtcraftRouterError> {
match image_inputs {
None => Ok(None),
Some(ImageListRef::Urls(urls)) => Ok(urls.into_iter().next()),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Schnell,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> Flux1SchnellTextToImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFlux1Schnell(
FalFlux1SchnellRequestState::TextToImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected TextToImage variant")
};
req
}
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> Flux1SchnellEditImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalFlux1Schnell(
FalFlux1SchnellRequestState::EditImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected EditImage variant")
};
req
}
// ── Mode detection ──
mod mode_detection {
use super::*;
#[test]
fn no_images_yields_text_to_image() {
let req = unwrap_t2i(build_fal_flux_1_schnell(base_builder()));
assert_eq!(req.prompt, "a cat in space");
}
#[test]
fn single_url_yields_edit_image() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert_eq!(req.image_url, "https://example.com/img.jpg");
}
#[test]
fn multiple_urls_takes_first() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec![
"https://example.com/a.jpg".to_string(),
"https://example.com/b.jpg".to_string(),
])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert_eq!(req.image_url, "https://example.com/a.jpg");
}
#[test]
fn empty_urls_yields_text_to_image() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec![])),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert_eq!(req.prompt, "a cat in space");
}
#[test]
fn media_tokens_return_error() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
..base_builder()
};
assert!(matches!(
build_fal_flux_1_schnell(builder),
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
}
// ── Num images ──
mod num_images_tests {
use super::*;
#[test]
fn default_is_one() {
let req = unwrap_t2i(build_fal_flux_1_schnell(base_builder()));
assert!(matches!(req.num_images, Flux1SchnellTextToImageNumImages::One));
}
#[test]
fn explicit_three() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert!(matches!(req.num_images, Flux1SchnellTextToImageNumImages::Three));
}
#[test]
fn zero_is_error() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(0),
..base_builder()
};
assert!(matches!(
build_fal_flux_1_schnell(builder),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
#[test]
fn over_four_error_out() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
..base_builder()
};
assert!(matches!(
build_fal_flux_1_schnell(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn over_four_clamps_with_upgrade() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert!(matches!(req.num_images, Flux1SchnellTextToImageNumImages::Four));
}
#[test]
fn edit_mode_num_images() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
image_batch_count: Some(2),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert!(matches!(req.num_images, Flux1SchnellEditImageNumImages::Two));
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn default_is_square_hd() {
let req = unwrap_t2i(build_fal_flux_1_schnell(base_builder()));
assert!(matches!(req.aspect_ratio, Flux1SchnellTextToImageAspectRatio::SquareHd));
}
#[test]
fn square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert!(matches!(req.aspect_ratio, Flux1SchnellTextToImageAspectRatio::Square));
}
#[test]
fn wide_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert!(matches!(req.aspect_ratio, Flux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine));
}
#[test]
fn tall_nine_by_sixteen() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert!(matches!(req.aspect_ratio, Flux1SchnellTextToImageAspectRatio::PortraitNineBySixteen));
}
#[test]
fn edit_auto_yields_none() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert!(req.image_size.is_none());
}
#[test]
fn edit_square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert!(matches!(req.image_size, Some(Flux1SchnellEditImageSize::Square)));
}
#[test]
fn edit_landscape() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert!(matches!(req.image_size, Some(Flux1SchnellEditImageSize::LandscapeSixteenByNine)));
}
#[test]
fn edit_no_aspect_ratio_yields_none() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: None,
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
assert!(req.image_size.is_none());
}
}
// ── Prompt ──
mod prompt_tests {
use super::*;
#[test]
fn prompt_is_passed_through() {
let builder = GenerateImageRequestBuilder {
prompt: Some("my custom prompt".to_string()),
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert_eq!(req.prompt, "my custom prompt");
}
#[test]
fn missing_prompt_defaults_to_empty() {
let builder = GenerateImageRequestBuilder {
prompt: None,
..base_builder()
};
let req = unwrap_t2i(build_fal_flux_1_schnell(builder));
assert_eq!(req.prompt, "");
}
}
}
@@ -0,0 +1,112 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
pub struct FalFlux1SchnellCostState {
cost_in_usd_cents: u64,
}
impl FalFlux1SchnellCostState {
pub fn from_request(request: &FalFlux1SchnellRequestState) -> Self {
let cost_in_usd_cents = match request {
FalFlux1SchnellRequestState::TextToImage(req) => req.calculate_cost_in_cents(),
FalFlux1SchnellRequestState::EditImage(req) => req.calculate_cost_in_cents(),
};
Self { cost_in_usd_cents }
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
ImageGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::requests::api::image::edit::flux_1_schnell_edit_image::api::{
Flux1SchnellEditImageNumImages, Flux1SchnellEditImageRequest,
};
use fal_client::requests::api::image::text::flux_1_schnell_text_to_image::api::{
Flux1SchnellTextToImageAspectRatio, Flux1SchnellTextToImageNumImages,
Flux1SchnellTextToImageRequest,
};
mod text_to_image_costs {
use super::*;
fn make_t2i(num_images: Flux1SchnellTextToImageNumImages) -> FalFlux1SchnellRequestState {
FalFlux1SchnellRequestState::TextToImage(Flux1SchnellTextToImageRequest {
prompt: "test".to_string(),
num_images,
aspect_ratio: Flux1SchnellTextToImageAspectRatio::Square,
})
}
#[test]
fn one_image() {
let cost = FalFlux1SchnellCostState::from_request(&make_t2i(Flux1SchnellTextToImageNumImages::One));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(1));
}
#[test]
fn two_images() {
let cost = FalFlux1SchnellCostState::from_request(&make_t2i(Flux1SchnellTextToImageNumImages::Two));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(2));
}
#[test]
fn four_images() {
let cost = FalFlux1SchnellCostState::from_request(&make_t2i(Flux1SchnellTextToImageNumImages::Four));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(4));
}
}
mod edit_image_costs {
use super::*;
fn make_edit(num_images: Flux1SchnellEditImageNumImages) -> FalFlux1SchnellRequestState {
FalFlux1SchnellRequestState::EditImage(Flux1SchnellEditImageRequest {
image_url: "https://example.com/img.jpg".to_string(),
num_images,
image_size: None,
})
}
#[test]
fn one_image() {
let cost = FalFlux1SchnellCostState::from_request(&make_edit(Flux1SchnellEditImageNumImages::One));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(1));
}
#[test]
fn three_images() {
let cost = FalFlux1SchnellCostState::from_request(&make_edit(Flux1SchnellEditImageNumImages::Three));
assert_eq!(cost.estimate_cost().cost_in_usd_cents, Some(3));
}
}
#[test]
fn cost_flags_are_correct() {
let state = FalFlux1SchnellCostState::from_request(
&FalFlux1SchnellRequestState::TextToImage(Flux1SchnellTextToImageRequest {
prompt: "test".to_string(),
num_images: Flux1SchnellTextToImageNumImages::One,
aspect_ratio: Flux1SchnellTextToImageAspectRatio::Square,
}),
);
let cost = state.estimate_cost();
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert!(cost.cost_in_credits.is_none());
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,172 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::api::image::edit::flux_1_schnell_edit_image::api::Flux1SchnellEditImageRequest;
use fal_client::requests::api::image::text::flux_1_schnell_text_to_image::api::Flux1SchnellTextToImageRequest;
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
#[derive(Clone, Debug)]
pub enum FalFlux1SchnellRequestState {
TextToImage(Flux1SchnellTextToImageRequest),
EditImage(Flux1SchnellEditImageRequest),
}
impl FalFlux1SchnellRequestState {
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
Self::TextToImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
Self::EditImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
}
}
}
// ── Helpers ──
struct FalResponseIds {
request_id: Option<String>,
gateway_request_id: Option<String>,
}
async fn send_fal_request<T: FalEndpoint>(
request: &T,
client: &RouterFalWebhookOptionalClient,
) -> Result<FalResponseIds, ArtcraftRouterError> {
if let Some(webhook_url) = &client.webhook_url {
let response = request
.send_webhook_request(&client.api_key, webhook_url)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: response.request_id,
gateway_request_id: response.gateway_request_id,
})
} else {
let response = request
.send_queue_request(&client.api_key)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: Some(response.request_id),
gateway_request_id: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::api::image::edit::flux_1_schnell_edit_image::api::Flux1SchnellEditImageNumImages;
use fal_client::requests::api::image::text::flux_1_schnell_text_to_image::api::{
Flux1SchnellTextToImageAspectRatio, Flux1SchnellTextToImageNumImages,
};
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
fn read_fal_api_key() -> FalApiKey {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read fal_api_key.txt");
FalApiKey::from_str(secret.trim())
}
fn client_with_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new_with_webhook(
read_fal_api_key(),
"https://example.com/fal-webhook-test".to_string(),
)
}
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new(read_fal_api_key())
}
// ── Text-to-image ──
mod text_to_image {
use super::*;
fn t2i_request() -> FalFlux1SchnellRequestState {
FalFlux1SchnellRequestState::TextToImage(Flux1SchnellTextToImageRequest {
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
num_images: Flux1SchnellTextToImageNumImages::One,
aspect_ratio: Flux1SchnellTextToImageAspectRatio::LandscapeSixteenByNine,
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let response = t2i_request().send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let response = t2i_request().send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue t2i — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
// ── Edit image ──
mod edit_image {
use super::*;
fn edit_request() -> FalFlux1SchnellRequestState {
FalFlux1SchnellRequestState::EditImage(Flux1SchnellEditImageRequest {
image_url: JUNO_AT_LAKE_IMAGE_URL.to_string(),
num_images: Flux1SchnellEditImageNumImages::One,
image_size: None,
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let response = edit_request().send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook edit — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let response = edit_request().send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue edit — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
}
@@ -0,0 +1,4 @@
pub mod flux_1_dev;
pub mod flux_1_schnell;
pub mod nano_banana_2;
pub mod nano_banana_pro;
@@ -0,0 +1,539 @@
use fal_client::requests::api::image::edit::nano_banana_2_edit_image::api::{
NanoBanana2EditImageAspectRatio, NanoBanana2EditImageNumImages,
NanoBanana2EditImageRequest, NanoBanana2EditImageResolution,
};
use fal_client::requests::api::image::text::nano_banana_2_text_to_image::api::{
NanoBanana2TextToImageAspectRatio, NanoBanana2TextToImageNumImages,
NanoBanana2TextToImageRequest, NanoBanana2TextToImageResolution,
};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
pub fn build_fal_nano_banana_2(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let prompt = builder.prompt.clone().unwrap_or_default();
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
let resolution = plan_resolution(builder.resolution);
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
let state = if image_urls.is_empty() {
// Text-to-image
let aspect_ratio = plan_t2i_aspect_ratio(builder.aspect_ratio);
FalNanoBanana2RequestState::TextToImage(NanoBanana2TextToImageRequest {
prompt,
num_images: to_t2i_num_images(num_images),
resolution: resolution.map(to_t2i_resolution),
aspect_ratio,
})
} else {
// Edit image
let aspect_ratio = plan_edit_aspect_ratio(builder.aspect_ratio);
FalNanoBanana2RequestState::EditImage(NanoBanana2EditImageRequest {
prompt,
image_urls,
num_images: to_edit_num_images(num_images),
resolution: resolution.map(to_edit_resolution),
aspect_ratio,
})
};
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalNanoBanana2(state),
))
}
// ── Num images ──
#[derive(Copy, Clone, Debug)]
enum PlannedNumImages {
One,
Two,
Three,
Four,
}
fn plan_num_images(
count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<PlannedNumImages, ArtcraftRouterError> {
let count = count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(PlannedNumImages::One),
2 => Ok(PlannedNumImages::Two),
3 => Ok(PlannedNumImages::Three),
4 => Ok(PlannedNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(PlannedNumImages::Four),
},
}
}
fn to_t2i_num_images(n: PlannedNumImages) -> NanoBanana2TextToImageNumImages {
match n {
PlannedNumImages::One => NanoBanana2TextToImageNumImages::One,
PlannedNumImages::Two => NanoBanana2TextToImageNumImages::Two,
PlannedNumImages::Three => NanoBanana2TextToImageNumImages::Three,
PlannedNumImages::Four => NanoBanana2TextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: PlannedNumImages) -> NanoBanana2EditImageNumImages {
match n {
PlannedNumImages::One => NanoBanana2EditImageNumImages::One,
PlannedNumImages::Two => NanoBanana2EditImageNumImages::Two,
PlannedNumImages::Three => NanoBanana2EditImageNumImages::Three,
PlannedNumImages::Four => NanoBanana2EditImageNumImages::Four,
}
}
// ── Resolution ──
#[derive(Copy, Clone, Debug)]
enum PlannedResolution {
HalfK,
OneK,
TwoK,
FourK,
}
fn plan_resolution(resolution: Option<CommonResolution>) -> Option<PlannedResolution> {
resolution.map(|r| match r {
CommonResolution::HalfK | CommonResolution::FourEightyP => PlannedResolution::HalfK,
CommonResolution::OneK | CommonResolution::SevenTwentyP | CommonResolution::TenEightyP => PlannedResolution::OneK,
CommonResolution::TwoK => PlannedResolution::TwoK,
CommonResolution::ThreeK | CommonResolution::FourK => PlannedResolution::FourK,
})
}
fn to_t2i_resolution(r: PlannedResolution) -> NanoBanana2TextToImageResolution {
match r {
PlannedResolution::HalfK => NanoBanana2TextToImageResolution::HalfK,
PlannedResolution::OneK => NanoBanana2TextToImageResolution::OneK,
PlannedResolution::TwoK => NanoBanana2TextToImageResolution::TwoK,
PlannedResolution::FourK => NanoBanana2TextToImageResolution::FourK,
}
}
fn to_edit_resolution(r: PlannedResolution) -> NanoBanana2EditImageResolution {
match r {
PlannedResolution::HalfK => NanoBanana2EditImageResolution::HalfK,
PlannedResolution::OneK => NanoBanana2EditImageResolution::OneK,
PlannedResolution::TwoK => NanoBanana2EditImageResolution::TwoK,
PlannedResolution::FourK => NanoBanana2EditImageResolution::FourK,
}
}
// ── Aspect ratio ──
fn plan_t2i_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
) -> Option<NanoBanana2TextToImageAspectRatio> {
use NanoBanana2TextToImageAspectRatio as T;
aspect_ratio.map(|ar| match ar {
CommonAspectRatio::Auto | CommonAspectRatio::Auto2k
| CommonAspectRatio::Auto3k | CommonAspectRatio::Auto4k => T::Auto,
CommonAspectRatio::Square | CommonAspectRatio::SquareHd => T::OneByOne,
CommonAspectRatio::WideFiveByFour => T::FiveByFour,
CommonAspectRatio::WideFourByThree => T::FourByThree,
CommonAspectRatio::WideThreeByTwo | CommonAspectRatio::Wide => T::ThreeByTwo,
CommonAspectRatio::WideSixteenByNine => T::SixteenByNine,
CommonAspectRatio::WideTwentyOneByNine => T::TwentyOneByNine,
CommonAspectRatio::TallFourByFive => T::FourByFive,
CommonAspectRatio::TallThreeByFour => T::ThreeByFour,
CommonAspectRatio::TallTwoByThree | CommonAspectRatio::Tall => T::TwoByThree,
CommonAspectRatio::TallNineBySixteen => T::NineBySixteen,
CommonAspectRatio::TallNineByTwentyOne => T::NineBySixteen, // nearest match
})
}
fn plan_edit_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
) -> Option<NanoBanana2EditImageAspectRatio> {
use NanoBanana2EditImageAspectRatio as E;
aspect_ratio.map(|ar| match ar {
CommonAspectRatio::Auto | CommonAspectRatio::Auto2k
| CommonAspectRatio::Auto3k | CommonAspectRatio::Auto4k => E::Auto,
CommonAspectRatio::Square | CommonAspectRatio::SquareHd => E::OneByOne,
CommonAspectRatio::WideFiveByFour => E::FiveByFour,
CommonAspectRatio::WideFourByThree => E::FourByThree,
CommonAspectRatio::WideThreeByTwo | CommonAspectRatio::Wide => E::ThreeByTwo,
CommonAspectRatio::WideSixteenByNine => E::SixteenByNine,
CommonAspectRatio::WideTwentyOneByNine => E::TwentyOneByNine,
CommonAspectRatio::TallFourByFive => E::FourByFive,
CommonAspectRatio::TallThreeByFour => E::ThreeByFour,
CommonAspectRatio::TallTwoByThree | CommonAspectRatio::Tall => E::TwoByThree,
CommonAspectRatio::TallNineBySixteen => E::NineBySixteen,
CommonAspectRatio::TallNineByTwentyOne => E::NineBySixteen, // nearest match
})
}
// ── Image inputs ──
fn resolve_image_urls(
image_inputs: Option<ImageListRef>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_inputs {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBanana2,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> NanoBanana2TextToImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalNanoBanana2(
FalNanoBanana2RequestState::TextToImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected TextToImage variant")
};
req
}
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> NanoBanana2EditImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalNanoBanana2(
FalNanoBanana2RequestState::EditImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected EditImage variant")
};
req
}
// ── Mode detection ──
mod mode_detection {
use super::*;
#[test]
fn no_images_yields_text_to_image() {
let result = build_fal_nano_banana_2(base_builder());
let req = unwrap_t2i(result);
assert_eq!(req.prompt, "a cat in space");
}
#[test]
fn urls_yield_edit_image() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let result = build_fal_nano_banana_2(builder);
let req = unwrap_edit(result);
assert_eq!(req.image_urls, vec!["https://example.com/img.jpg"]);
}
#[test]
fn media_tokens_return_error() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
..base_builder()
};
let result = build_fal_nano_banana_2(builder);
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
}
// ── Num images ──
mod num_images_tests {
use super::*;
#[test]
fn default_is_one() {
let req = unwrap_t2i(build_fal_nano_banana_2(base_builder()));
assert!(matches!(req.num_images, NanoBanana2TextToImageNumImages::One));
}
#[test]
fn explicit_three() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.num_images, NanoBanana2TextToImageNumImages::Three));
}
#[test]
fn zero_is_error() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(0),
..base_builder()
};
assert!(matches!(
build_fal_nano_banana_2(builder),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
#[test]
fn over_four_error_out() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
..base_builder()
};
assert!(matches!(
build_fal_nano_banana_2(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn over_four_clamps_with_upgrade() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.num_images, NanoBanana2TextToImageNumImages::Four));
}
#[test]
fn edit_mode_num_images() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
image_batch_count: Some(2),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_2(builder));
assert!(matches!(req.num_images, NanoBanana2EditImageNumImages::Two));
}
}
// ── Resolution ──
mod resolution_tests {
use super::*;
#[test]
fn none_is_none() {
let req = unwrap_t2i(build_fal_nano_banana_2(base_builder()));
assert!(req.resolution.is_none());
}
#[test]
fn half_k_maps_to_half_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::HalfK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::HalfK)));
}
#[test]
fn four_eighty_p_maps_to_half_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::FourEightyP),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::HalfK)));
}
#[test]
fn one_k_maps_to_one_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::OneK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::OneK)));
}
#[test]
fn two_k_maps_to_two_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::TwoK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::TwoK)));
}
#[test]
fn four_k_maps_to_four_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::FourK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::FourK)));
}
#[test]
fn three_k_rounds_up_to_four_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::ThreeK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::FourK)));
}
#[test]
fn edit_mode_resolution() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
resolution: Some(CommonResolution::HalfK),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_2(builder));
assert!(matches!(req.resolution, Some(NanoBanana2EditImageResolution::HalfK)));
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn none_is_none_t2i() {
let req = unwrap_t2i(build_fal_nano_banana_2(base_builder()));
assert!(req.aspect_ratio.is_none());
}
#[test]
fn auto_maps_to_auto_t2i() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBanana2TextToImageAspectRatio::Auto)));
}
#[test]
fn square_maps_to_one_by_one() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBanana2TextToImageAspectRatio::OneByOne)));
}
#[test]
fn wide_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBanana2TextToImageAspectRatio::SixteenByNine)));
}
#[test]
fn tall_nine_by_sixteen() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBanana2TextToImageAspectRatio::NineBySixteen)));
}
#[test]
fn edit_auto() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_2(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBanana2EditImageAspectRatio::Auto)));
}
#[test]
fn edit_square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_2(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBanana2EditImageAspectRatio::OneByOne)));
}
}
// ── Prompt ──
mod prompt_tests {
use super::*;
#[test]
fn prompt_is_passed_through() {
let builder = GenerateImageRequestBuilder {
prompt: Some("my custom prompt".to_string()),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert_eq!(req.prompt, "my custom prompt");
}
#[test]
fn missing_prompt_defaults_to_empty() {
let builder = GenerateImageRequestBuilder {
prompt: None,
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
assert_eq!(req.prompt, "");
}
}
}
@@ -0,0 +1,165 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
pub struct FalNanoBanana2CostState {
cost_in_usd_cents: u64,
}
impl FalNanoBanana2CostState {
pub fn from_request(request: &FalNanoBanana2RequestState) -> Self {
let cost_in_usd_cents = match request {
FalNanoBanana2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
FalNanoBanana2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
};
Self { cost_in_usd_cents }
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
ImageGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::requests::api::image::edit::nano_banana_2_edit_image::api::{
NanoBanana2EditImageNumImages, NanoBanana2EditImageRequest,
NanoBanana2EditImageResolution,
};
use fal_client::requests::api::image::text::nano_banana_2_text_to_image::api::{
NanoBanana2TextToImageNumImages, NanoBanana2TextToImageRequest,
NanoBanana2TextToImageResolution,
};
mod text_to_image_costs {
use super::*;
fn make_t2i(
num_images: NanoBanana2TextToImageNumImages,
resolution: Option<NanoBanana2TextToImageResolution>,
) -> FalNanoBanana2RequestState {
FalNanoBanana2RequestState::TextToImage(NanoBanana2TextToImageRequest {
prompt: "test".to_string(),
num_images,
resolution,
aspect_ratio: None,
})
}
#[test]
fn one_image_default_resolution() {
let state = FalNanoBanana2CostState::from_request(
&make_t2i(NanoBanana2TextToImageNumImages::One, None),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
}
#[test]
fn one_image_half_k() {
let state = FalNanoBanana2CostState::from_request(
&make_t2i(NanoBanana2TextToImageNumImages::One, Some(NanoBanana2TextToImageResolution::HalfK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
}
#[test]
fn four_images_one_k() {
let state = FalNanoBanana2CostState::from_request(
&make_t2i(NanoBanana2TextToImageNumImages::Four, Some(NanoBanana2TextToImageResolution::OneK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
}
#[test]
fn one_image_four_k() {
let state = FalNanoBanana2CostState::from_request(
&make_t2i(NanoBanana2TextToImageNumImages::One, Some(NanoBanana2TextToImageResolution::FourK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
}
#[test]
fn two_images_four_k() {
let state = FalNanoBanana2CostState::from_request(
&make_t2i(NanoBanana2TextToImageNumImages::Two, Some(NanoBanana2TextToImageResolution::FourK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
}
}
mod edit_image_costs {
use super::*;
fn make_edit(
num_images: NanoBanana2EditImageNumImages,
resolution: Option<NanoBanana2EditImageResolution>,
) -> FalNanoBanana2RequestState {
FalNanoBanana2RequestState::EditImage(NanoBanana2EditImageRequest {
prompt: "test".to_string(),
image_urls: vec!["https://example.com/image.jpg".to_string()],
num_images,
resolution,
aspect_ratio: None,
})
}
#[test]
fn one_image_default_resolution() {
let state = FalNanoBanana2CostState::from_request(
&make_edit(NanoBanana2EditImageNumImages::One, None),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
}
#[test]
fn one_image_half_k() {
let state = FalNanoBanana2CostState::from_request(
&make_edit(NanoBanana2EditImageNumImages::One, Some(NanoBanana2EditImageResolution::HalfK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
}
#[test]
fn three_images_two_k() {
let state = FalNanoBanana2CostState::from_request(
&make_edit(NanoBanana2EditImageNumImages::Three, Some(NanoBanana2EditImageResolution::TwoK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(45));
}
#[test]
fn one_image_four_k() {
let state = FalNanoBanana2CostState::from_request(
&make_edit(NanoBanana2EditImageNumImages::One, Some(NanoBanana2EditImageResolution::FourK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
}
}
#[test]
fn cost_flags_are_correct() {
let state = FalNanoBanana2CostState::from_request(
&FalNanoBanana2RequestState::TextToImage(NanoBanana2TextToImageRequest {
prompt: "test".to_string(),
num_images: NanoBanana2TextToImageNumImages::One,
resolution: None,
aspect_ratio: None,
}),
);
let cost = state.estimate_cost();
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert!(cost.cost_in_credits.is_none());
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,187 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::api::image::edit::nano_banana_2_edit_image::api::NanoBanana2EditImageRequest;
use fal_client::requests::api::image::text::nano_banana_2_text_to_image::api::NanoBanana2TextToImageRequest;
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
#[derive(Clone, Debug)]
pub enum FalNanoBanana2RequestState {
TextToImage(NanoBanana2TextToImageRequest),
EditImage(NanoBanana2EditImageRequest),
}
impl FalNanoBanana2RequestState {
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
Self::TextToImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
Self::EditImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
}
}
}
// ── Helpers ──
struct FalResponseIds {
request_id: Option<String>,
gateway_request_id: Option<String>,
}
async fn send_fal_request<T: FalEndpoint>(
request: &T,
client: &RouterFalWebhookOptionalClient,
) -> Result<FalResponseIds, ArtcraftRouterError> {
if let Some(webhook_url) = &client.webhook_url {
let response = request
.send_webhook_request(&client.api_key, webhook_url)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: response.request_id,
gateway_request_id: response.gateway_request_id,
})
} else {
let response = request
.send_queue_request(&client.api_key)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: Some(response.request_id),
gateway_request_id: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::api::image::edit::nano_banana_2_edit_image::api::{
NanoBanana2EditImageAspectRatio, NanoBanana2EditImageNumImages,
NanoBanana2EditImageResolution,
};
use fal_client::requests::api::image::text::nano_banana_2_text_to_image::api::{
NanoBanana2TextToImageAspectRatio, NanoBanana2TextToImageNumImages,
NanoBanana2TextToImageResolution,
};
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
fn read_fal_api_key() -> FalApiKey {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read fal_api_key.txt");
FalApiKey::from_str(secret.trim())
}
fn client_with_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new_with_webhook(
read_fal_api_key(),
"https://example.com/fal-webhook-test".to_string(),
)
}
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new(read_fal_api_key())
}
// ── Text-to-image ──
mod text_to_image {
use super::*;
fn t2i_request() -> FalNanoBanana2RequestState {
FalNanoBanana2RequestState::TextToImage(NanoBanana2TextToImageRequest {
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
num_images: NanoBanana2TextToImageNumImages::One,
resolution: Some(NanoBanana2TextToImageResolution::OneK),
aspect_ratio: Some(NanoBanana2TextToImageAspectRatio::SixteenByNine),
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let state = t2i_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let state = t2i_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue t2i — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
// ── Edit image ──
mod edit_image {
use super::*;
use test_data::web::image_urls::WHITE_HOUSE_SUNSET_IMAGE_URL;
fn edit_request() -> FalNanoBanana2RequestState {
FalNanoBanana2RequestState::EditImage(NanoBanana2EditImageRequest {
prompt: "add a party hat to the dog, and put the dog in front of the location".to_string(),
image_urls: vec![
JUNO_AT_LAKE_IMAGE_URL.to_string(),
WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(),
],
num_images: NanoBanana2EditImageNumImages::One,
resolution: Some(NanoBanana2EditImageResolution::OneK),
aspect_ratio: Some(NanoBanana2EditImageAspectRatio::SixteenByNine),
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let state = edit_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook edit — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let state = edit_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue edit — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
}
@@ -0,0 +1,526 @@
use fal_client::requests::api::image::edit::nano_banana_pro_edit_image::api::{
NanoBananaProEditImageAspectRatio, NanoBananaProEditImageNumImages,
NanoBananaProEditImageRequest, NanoBananaProEditImageResolution,
};
use fal_client::requests::api::image::text::nano_banana_pro_text_to_image::api::{
NanoBananaProTextToImageAspectRatio, NanoBananaProTextToImageNumImages,
NanoBananaProTextToImageRequest, NanoBananaProTextToImageResolution,
};
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
pub fn build_fal_nano_banana_pro(
builder: GenerateImageRequestBuilder,
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let strategy = builder.request_mismatch_mitigation_strategy;
let prompt = builder.prompt.clone().unwrap_or_default();
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
let resolution = plan_resolution(builder.resolution);
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
let state = if image_urls.is_empty() {
// Text-to-image
let aspect_ratio = plan_t2i_aspect_ratio(builder.aspect_ratio);
FalNanoBananaProRequestState::TextToImage(NanoBananaProTextToImageRequest {
prompt,
num_images: to_t2i_num_images(num_images),
resolution: resolution.map(to_t2i_resolution),
aspect_ratio,
})
} else {
// Edit image
let aspect_ratio = plan_edit_aspect_ratio(builder.aspect_ratio);
FalNanoBananaProRequestState::EditImage(NanoBananaProEditImageRequest {
prompt,
image_urls,
num_images: to_edit_num_images(num_images),
resolution: resolution.map(to_edit_resolution),
aspect_ratio,
})
};
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalNanoBananaPro(state),
))
}
// ── Num images ──
#[derive(Copy, Clone, Debug)]
enum PlannedNumImages {
One,
Two,
Three,
Four,
}
fn plan_num_images(
count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<PlannedNumImages, ArtcraftRouterError> {
let count = count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(PlannedNumImages::One),
2 => Ok(PlannedNumImages::Two),
3 => Ok(PlannedNumImages::Three),
4 => Ok(PlannedNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(PlannedNumImages::Four),
},
}
}
fn to_t2i_num_images(n: PlannedNumImages) -> NanoBananaProTextToImageNumImages {
match n {
PlannedNumImages::One => NanoBananaProTextToImageNumImages::One,
PlannedNumImages::Two => NanoBananaProTextToImageNumImages::Two,
PlannedNumImages::Three => NanoBananaProTextToImageNumImages::Three,
PlannedNumImages::Four => NanoBananaProTextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: PlannedNumImages) -> NanoBananaProEditImageNumImages {
match n {
PlannedNumImages::One => NanoBananaProEditImageNumImages::One,
PlannedNumImages::Two => NanoBananaProEditImageNumImages::Two,
PlannedNumImages::Three => NanoBananaProEditImageNumImages::Three,
PlannedNumImages::Four => NanoBananaProEditImageNumImages::Four,
}
}
// ── Resolution ──
#[derive(Copy, Clone, Debug)]
enum PlannedResolution {
OneK,
TwoK,
FourK,
}
fn plan_resolution(resolution: Option<CommonResolution>) -> Option<PlannedResolution> {
resolution.map(|r| match r {
CommonResolution::HalfK | CommonResolution::FourEightyP | CommonResolution::SevenTwentyP
| CommonResolution::OneK | CommonResolution::TenEightyP => PlannedResolution::OneK,
CommonResolution::TwoK => PlannedResolution::TwoK,
CommonResolution::ThreeK | CommonResolution::FourK => PlannedResolution::FourK,
})
}
fn to_t2i_resolution(r: PlannedResolution) -> NanoBananaProTextToImageResolution {
match r {
PlannedResolution::OneK => NanoBananaProTextToImageResolution::OneK,
PlannedResolution::TwoK => NanoBananaProTextToImageResolution::TwoK,
PlannedResolution::FourK => NanoBananaProTextToImageResolution::FourK,
}
}
fn to_edit_resolution(r: PlannedResolution) -> NanoBananaProEditImageResolution {
match r {
PlannedResolution::OneK => NanoBananaProEditImageResolution::OneK,
PlannedResolution::TwoK => NanoBananaProEditImageResolution::TwoK,
PlannedResolution::FourK => NanoBananaProEditImageResolution::FourK,
}
}
// ── Aspect ratio ──
fn plan_t2i_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
) -> Option<NanoBananaProTextToImageAspectRatio> {
use NanoBananaProTextToImageAspectRatio as T;
aspect_ratio.and_then(|ar| match ar {
CommonAspectRatio::Square | CommonAspectRatio::SquareHd => Some(T::OneByOne),
CommonAspectRatio::WideFiveByFour => Some(T::FiveByFour),
CommonAspectRatio::WideFourByThree => Some(T::FourByThree),
CommonAspectRatio::WideThreeByTwo | CommonAspectRatio::Wide => Some(T::ThreeByTwo),
CommonAspectRatio::WideSixteenByNine => Some(T::SixteenByNine),
CommonAspectRatio::WideTwentyOneByNine => Some(T::TwentyOneByNine),
CommonAspectRatio::TallFourByFive => Some(T::FourByFive),
CommonAspectRatio::TallThreeByFour => Some(T::ThreeByFour),
CommonAspectRatio::TallTwoByThree | CommonAspectRatio::Tall => Some(T::TwoByThree),
CommonAspectRatio::TallNineBySixteen => Some(T::NineBySixteen),
CommonAspectRatio::TallNineByTwentyOne => Some(T::NineBySixteen), // nearest match
CommonAspectRatio::Auto | CommonAspectRatio::Auto2k
| CommonAspectRatio::Auto3k | CommonAspectRatio::Auto4k => None,
})
}
fn plan_edit_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
) -> Option<NanoBananaProEditImageAspectRatio> {
use NanoBananaProEditImageAspectRatio as E;
aspect_ratio.map(|ar| match ar {
CommonAspectRatio::Auto | CommonAspectRatio::Auto2k
| CommonAspectRatio::Auto3k | CommonAspectRatio::Auto4k => E::Auto,
CommonAspectRatio::Square | CommonAspectRatio::SquareHd => E::OneByOne,
CommonAspectRatio::WideFiveByFour => E::FiveByFour,
CommonAspectRatio::WideFourByThree => E::FourByThree,
CommonAspectRatio::WideThreeByTwo | CommonAspectRatio::Wide => E::ThreeByTwo,
CommonAspectRatio::WideSixteenByNine => E::SixteenByNine,
CommonAspectRatio::WideTwentyOneByNine => E::TwentyOneByNine,
CommonAspectRatio::TallFourByFive => E::FourByFive,
CommonAspectRatio::TallThreeByFour => E::ThreeByFour,
CommonAspectRatio::TallTwoByThree | CommonAspectRatio::Tall => E::TwoByThree,
CommonAspectRatio::TallNineBySixteen => E::NineBySixteen,
CommonAspectRatio::TallNineByTwentyOne => E::NineBySixteen, // nearest match
})
}
// ── Image inputs ──
fn resolve_image_urls(
image_inputs: Option<ImageListRef>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_inputs {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
fn base_builder() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> NanoBananaProTextToImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalNanoBananaPro(
FalNanoBananaProRequestState::TextToImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected TextToImage variant")
};
req
}
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> NanoBananaProEditImageRequest {
let ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::FalNanoBananaPro(
FalNanoBananaProRequestState::EditImage(req)
)
) = result.expect("build should succeed") else {
panic!("expected EditImage variant")
};
req
}
// ── Mode detection ──
mod mode_detection {
use super::*;
#[test]
fn no_images_yields_text_to_image() {
let result = build_fal_nano_banana_pro(base_builder());
let req = unwrap_t2i(result);
assert_eq!(req.prompt, "a cat in space");
}
#[test]
fn urls_yield_edit_image() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let result = build_fal_nano_banana_pro(builder);
let req = unwrap_edit(result);
assert_eq!(req.image_urls, vec!["https://example.com/img.jpg"]);
}
#[test]
fn media_tokens_return_error() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
..base_builder()
};
let result = build_fal_nano_banana_pro(builder);
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
}
// ── Num images ──
mod num_images_tests {
use super::*;
#[test]
fn default_is_one() {
let req = unwrap_t2i(build_fal_nano_banana_pro(base_builder()));
assert!(matches!(req.num_images, NanoBananaProTextToImageNumImages::One));
}
#[test]
fn explicit_three() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(3),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.num_images, NanoBananaProTextToImageNumImages::Three));
}
#[test]
fn zero_is_error() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(0),
..base_builder()
};
assert!(matches!(
build_fal_nano_banana_pro(builder),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
#[test]
fn over_four_error_out() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
..base_builder()
};
assert!(matches!(
build_fal_nano_banana_pro(builder),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn over_four_clamps_with_upgrade() {
let builder = GenerateImageRequestBuilder {
image_batch_count: Some(7),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.num_images, NanoBananaProTextToImageNumImages::Four));
}
#[test]
fn edit_mode_num_images() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
image_batch_count: Some(2),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_pro(builder));
assert!(matches!(req.num_images, NanoBananaProEditImageNumImages::Two));
}
}
// ── Resolution ──
mod resolution_tests {
use super::*;
#[test]
fn none_is_none() {
let req = unwrap_t2i(build_fal_nano_banana_pro(base_builder()));
assert!(req.resolution.is_none());
}
#[test]
fn one_k_maps_to_one_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::OneK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::OneK)));
}
#[test]
fn two_k_maps_to_two_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::TwoK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::TwoK)));
}
#[test]
fn four_k_maps_to_four_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::FourK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::FourK)));
}
#[test]
fn three_k_rounds_up_to_four_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::ThreeK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::FourK)));
}
#[test]
fn half_k_rounds_to_one_k() {
let builder = GenerateImageRequestBuilder {
resolution: Some(CommonResolution::HalfK),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::OneK)));
}
#[test]
fn edit_mode_resolution() {
let builder = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
resolution: Some(CommonResolution::FourK),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_pro(builder));
assert!(matches!(req.resolution, Some(NanoBananaProEditImageResolution::FourK)));
}
}
// ── Aspect ratio ──
mod aspect_ratio_tests {
use super::*;
#[test]
fn none_is_none_t2i() {
let req = unwrap_t2i(build_fal_nano_banana_pro(base_builder()));
assert!(req.aspect_ratio.is_none());
}
#[test]
fn square_maps_to_one_by_one() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBananaProTextToImageAspectRatio::OneByOne)));
}
#[test]
fn wide_sixteen_by_nine() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBananaProTextToImageAspectRatio::SixteenByNine)));
}
#[test]
fn tall_nine_by_sixteen() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBananaProTextToImageAspectRatio::NineBySixteen)));
}
#[test]
fn auto_yields_none_for_t2i() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert!(req.aspect_ratio.is_none());
}
#[test]
fn auto_yields_auto_for_edit() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_pro(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBananaProEditImageAspectRatio::Auto)));
}
#[test]
fn edit_square() {
let builder = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
..base_builder()
};
let req = unwrap_edit(build_fal_nano_banana_pro(builder));
assert!(matches!(req.aspect_ratio, Some(NanoBananaProEditImageAspectRatio::OneByOne)));
}
}
// ── Prompt ──
mod prompt_tests {
use super::*;
#[test]
fn prompt_is_passed_through() {
let builder = GenerateImageRequestBuilder {
prompt: Some("my custom prompt".to_string()),
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert_eq!(req.prompt, "my custom prompt");
}
#[test]
fn missing_prompt_defaults_to_empty() {
let builder = GenerateImageRequestBuilder {
prompt: None,
..base_builder()
};
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
assert_eq!(req.prompt, "");
}
}
}
@@ -0,0 +1,149 @@
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
pub struct FalNanoBananaProCostState {
cost_in_usd_cents: u64,
}
impl FalNanoBananaProCostState {
pub fn from_request(request: &FalNanoBananaProRequestState) -> Self {
let cost_in_usd_cents = match request {
FalNanoBananaProRequestState::TextToImage(req) => req.calculate_cost_in_cents(),
FalNanoBananaProRequestState::EditImage(req) => req.calculate_cost_in_cents(),
};
Self { cost_in_usd_cents }
}
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
ImageGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: Some(self.cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::requests::api::image::edit::nano_banana_pro_edit_image::api::{
NanoBananaProEditImageNumImages, NanoBananaProEditImageRequest,
NanoBananaProEditImageResolution,
};
use fal_client::requests::api::image::text::nano_banana_pro_text_to_image::api::{
NanoBananaProTextToImageNumImages, NanoBananaProTextToImageRequest,
NanoBananaProTextToImageResolution,
};
mod text_to_image_costs {
use super::*;
fn make_t2i(
num_images: NanoBananaProTextToImageNumImages,
resolution: Option<NanoBananaProTextToImageResolution>,
) -> FalNanoBananaProRequestState {
FalNanoBananaProRequestState::TextToImage(NanoBananaProTextToImageRequest {
prompt: "test".to_string(),
num_images,
resolution,
aspect_ratio: None,
})
}
#[test]
fn one_image_default_resolution() {
let state = FalNanoBananaProCostState::from_request(
&make_t2i(NanoBananaProTextToImageNumImages::One, None),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
}
#[test]
fn four_images_one_k() {
let state = FalNanoBananaProCostState::from_request(
&make_t2i(NanoBananaProTextToImageNumImages::Four, Some(NanoBananaProTextToImageResolution::OneK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
}
#[test]
fn one_image_four_k() {
let state = FalNanoBananaProCostState::from_request(
&make_t2i(NanoBananaProTextToImageNumImages::One, Some(NanoBananaProTextToImageResolution::FourK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
}
#[test]
fn two_images_four_k() {
let state = FalNanoBananaProCostState::from_request(
&make_t2i(NanoBananaProTextToImageNumImages::Two, Some(NanoBananaProTextToImageResolution::FourK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
}
}
mod edit_image_costs {
use super::*;
fn make_edit(
num_images: NanoBananaProEditImageNumImages,
resolution: Option<NanoBananaProEditImageResolution>,
) -> FalNanoBananaProRequestState {
FalNanoBananaProRequestState::EditImage(NanoBananaProEditImageRequest {
prompt: "test".to_string(),
image_urls: vec!["https://example.com/img.jpg".to_string()],
num_images,
resolution,
aspect_ratio: None,
})
}
#[test]
fn one_image_default_resolution() {
let state = FalNanoBananaProCostState::from_request(
&make_edit(NanoBananaProEditImageNumImages::One, None),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
}
#[test]
fn three_images_two_k() {
let state = FalNanoBananaProCostState::from_request(
&make_edit(NanoBananaProEditImageNumImages::Three, Some(NanoBananaProEditImageResolution::TwoK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(45));
}
#[test]
fn one_image_four_k() {
let state = FalNanoBananaProCostState::from_request(
&make_edit(NanoBananaProEditImageNumImages::One, Some(NanoBananaProEditImageResolution::FourK)),
);
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
}
}
#[test]
fn cost_flags_are_correct() {
let state = FalNanoBananaProCostState::from_request(
&FalNanoBananaProRequestState::TextToImage(NanoBananaProTextToImageRequest {
prompt: "test".to_string(),
num_images: NanoBananaProTextToImageNumImages::One,
resolution: None,
aspect_ratio: None,
}),
);
let cost = state.estimate_cost();
assert!(!cost.is_free);
assert!(!cost.is_unlimited);
assert!(!cost.is_rate_limited);
assert!(!cost.has_watermark);
assert!(cost.cost_in_credits.is_none());
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -0,0 +1,188 @@
use std::fmt::Debug;
use std::sync::Arc;
use fal_client::requests::api::image::edit::nano_banana_pro_edit_image::api::NanoBananaProEditImageRequest;
use fal_client::requests::api::image::text::nano_banana_pro_text_to_image::api::NanoBananaProTextToImageRequest;
use fal_client::requests::traits::fal_endpoint_trait::FalEndpoint;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
#[derive(Clone, Debug)]
pub enum FalNanoBananaProRequestState {
TextToImage(NanoBananaProTextToImageRequest),
EditImage(NanoBananaProEditImageRequest),
}
impl FalNanoBananaProRequestState {
pub async fn send(&self, client: &RouterFalWebhookOptionalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
Self::TextToImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
Self::EditImage(request) => {
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let payload = send_fal_request(request, client).await?;
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
request_id: payload.request_id,
gateway_request_id: payload.gateway_request_id,
maybe_outbound_request: Some(outbound),
}))
}
}
}
}
// ── Helpers ──
struct FalResponseIds {
request_id: Option<String>,
gateway_request_id: Option<String>,
}
/// Send a FAL request via webhook (if URL present) or queue (if not).
async fn send_fal_request<T: FalEndpoint>(
request: &T,
client: &RouterFalWebhookOptionalClient,
) -> Result<FalResponseIds, ArtcraftRouterError> {
if let Some(webhook_url) = &client.webhook_url {
let response = request
.send_webhook_request(&client.api_key, webhook_url)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: response.request_id,
gateway_request_id: response.gateway_request_id,
})
} else {
let response = request
.send_queue_request(&client.api_key)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
Ok(FalResponseIds {
request_id: Some(response.request_id),
gateway_request_id: None,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use fal_client::creds::fal_api_key::FalApiKey;
use fal_client::requests::api::image::edit::nano_banana_pro_edit_image::api::{
NanoBananaProEditImageAspectRatio, NanoBananaProEditImageNumImages,
NanoBananaProEditImageResolution,
};
use fal_client::requests::api::image::text::nano_banana_pro_text_to_image::api::{
NanoBananaProTextToImageAspectRatio, NanoBananaProTextToImageNumImages,
NanoBananaProTextToImageResolution,
};
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
fn read_fal_api_key() -> FalApiKey {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read fal_api_key.txt");
FalApiKey::from_str(secret.trim())
}
fn client_with_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new_with_webhook(
read_fal_api_key(),
"https://example.com/fal-webhook-test".to_string(),
)
}
fn client_without_webhook() -> RouterFalWebhookOptionalClient {
RouterFalWebhookOptionalClient::new(read_fal_api_key())
}
// ── Text-to-image ──
mod text_to_image {
use super::*;
fn t2i_request() -> FalNanoBananaProRequestState {
FalNanoBananaProRequestState::TextToImage(NanoBananaProTextToImageRequest {
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
num_images: NanoBananaProTextToImageNumImages::One,
resolution: Some(NanoBananaProTextToImageResolution::OneK),
aspect_ratio: Some(NanoBananaProTextToImageAspectRatio::SixteenByNine),
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let state = t2i_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let state = t2i_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue t2i — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
// ── Edit image ──
mod edit_image {
use super::*;
use test_data::web::image_urls::WHITE_HOUSE_SUNSET_IMAGE_URL;
fn edit_request() -> FalNanoBananaProRequestState {
FalNanoBananaProRequestState::EditImage(NanoBananaProEditImageRequest {
prompt: "add a party hat to the dog, and put the dog in front of the location".to_string(),
image_urls: vec![
JUNO_AT_LAKE_IMAGE_URL.to_string(),
WHITE_HOUSE_SUNSET_IMAGE_URL.to_string(),
],
num_images: NanoBananaProEditImageNumImages::One,
resolution: Some(NanoBananaProEditImageResolution::OneK),
aspect_ratio: Some(NanoBananaProEditImageAspectRatio::SixteenByNine),
})
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_webhook() {
let client = client_with_webhook();
let state = edit_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Webhook edit — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
}
#[tokio::test]
#[ignore] // requires real API key, incurs cost
async fn send_via_queue() {
let client = client_without_webhook();
let state = edit_request();
let response = state.send(&client).await.expect("send should succeed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Queue edit — request_id: {:?}", payload.request_id);
assert!(payload.request_id.is_some());
}
}
}
@@ -1,4 +1,5 @@
pub mod generate_image;
pub mod generate_image_v2;
pub mod generate_splat;
pub mod generate_video;
pub mod generate_video_v2;
@@ -5,7 +5,7 @@ use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigati
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_client::RouterClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;
@@ -25,8 +25,8 @@ pub fn get_artcraft_client() -> RouterClient {
get_artcraft_client_for_env(ApiHost::Storyteller)
}
pub fn base_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -52,8 +52,8 @@ pub fn get_fal_client() -> RouterClient {
RouterClient::Fal(RouterFalClient::new(api_key, webhook_url))
}
pub fn base_fal_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_fal_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Fal,
prompt: Some("a cat in space".to_string()),
@@ -71,8 +71,8 @@ pub fn base_fal_image_request() -> GenerateImageRequest {
}
}
pub fn base_seedream_4_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_seedream_4_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Seedream4,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -90,8 +90,8 @@ pub fn base_seedream_4_image_request() -> GenerateImageRequest {
}
}
pub fn base_seedream_4p5_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_seedream_4p5_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Seedream4p5,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -109,8 +109,8 @@ pub fn base_seedream_4p5_image_request() -> GenerateImageRequest {
}
}
pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_seedream_5_lite_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Seedream5Lite,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -128,8 +128,8 @@ pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest {
}
}
pub fn base_nano_banana_2_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_nano_banana_2_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBanana2,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -147,8 +147,8 @@ pub fn base_nano_banana_2_image_request() -> GenerateImageRequest {
}
}
pub fn base_nano_banana_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_nano_banana_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::NanoBanana,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -166,8 +166,8 @@ pub fn base_nano_banana_image_request() -> GenerateImageRequest {
}
}
pub fn base_gpt_image_1_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_gpt_image_1_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -185,8 +185,8 @@ pub fn base_gpt_image_1_image_request() -> GenerateImageRequest {
}
}
pub fn base_gpt_image_2_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_gpt_image_2_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage2,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -204,8 +204,8 @@ pub fn base_gpt_image_2_image_request() -> GenerateImageRequest {
}
}
pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1p5,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -223,8 +223,8 @@ pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequest {
}
}
pub fn base_flux_1_dev_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_flux_1_dev_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Dev,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -242,8 +242,8 @@ pub fn base_flux_1_dev_image_request() -> GenerateImageRequest {
}
}
pub fn base_flux_1_schnell_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_flux_1_schnell_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Schnell,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -261,8 +261,8 @@ pub fn base_flux_1_schnell_image_request() -> GenerateImageRequest {
}
}
pub fn base_flux_pro_1p1_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_flux_pro_1p1_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::FluxPro11,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -280,8 +280,8 @@ pub fn base_flux_pro_1p1_image_request() -> GenerateImageRequest {
}
}
pub fn base_qwen_edit_2511_angles_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_qwen_edit_2511_angles_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::QwenEdit2511Angles,
provider: Provider::Artcraft,
prompt: None,
@@ -299,8 +299,8 @@ pub fn base_qwen_edit_2511_angles_image_request() -> GenerateImageRequest {
}
}
pub fn base_flux_2_lora_angles_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_flux_2_lora_angles_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::Flux2LoraAngles,
provider: Provider::Artcraft,
prompt: None,
@@ -318,8 +318,8 @@ pub fn base_flux_2_lora_angles_image_request() -> GenerateImageRequest {
}
}
pub fn base_flux_pro_1p1_ultra_image_request() -> GenerateImageRequest {
GenerateImageRequest {
pub fn base_flux_pro_1p1_ultra_image_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::FluxPro11Ultra,
provider: Provider::Artcraft,
prompt: Some("a cat in space".to_string()),
@@ -24,6 +24,13 @@ pub enum BeebleSpecificApiError {
message: String,
webhook_url: String,
},
/// 400 Bad Request — the video has too many frames.
VideoHasTooManyFrames {
max_frames: u64,
detected_frames: u64,
message: String,
},
}
impl Error for BeebleSpecificApiError {}
@@ -39,6 +46,9 @@ impl Display for BeebleSpecificApiError {
Self::BadWebhookUrl { message, webhook_url } => {
write!(f, "Beeble API: Bad webhook URL '{}': {}", webhook_url, message)
}
Self::VideoHasTooManyFrames { max_frames, detected_frames, message } => {
write!(f, "Beeble API: Video has too many frames ({} detected, max {}): {}", detected_frames, max_frames, message)
}
}
}
}
@@ -28,6 +28,16 @@ pub fn handle_error_response<T>(
webhook_url: context.maybe_callback_url.unwrap_or_default().to_string(),
}.into())
}
400 if response_body.contains("VIDEO_TOO_MANY_FRAMES") => {
let message = extract_error_message(response_body)
.unwrap_or_else(|| "Video has too many frames".to_string());
let (detected, max) = extract_frame_counts(&message);
Err(BeebleSpecificApiError::VideoHasTooManyFrames {
max_frames: max,
detected_frames: detected,
message,
}.into())
}
409 => Err(BeebleSpecificApiError::IdempotencyConflict.into()),
429 => Err(BeebleSpecificApiError::RateLimited.into()),
_ => {
@@ -45,3 +55,18 @@ fn extract_error_message(body: &str) -> Option<String> {
let parsed: serde_json::Value = serde_json::from_str(body).ok()?;
parsed.get("error")?.get("message")?.as_str().map(|s| s.to_string())
}
/// Parse frame counts from a message like "Video has 419 frames, which exceeds the maximum of 240."
fn extract_frame_counts(message: &str) -> (u64, u64) {
let numbers: Vec<u64> = message
.split(|c: char| !c.is_ascii_digit())
.filter(|s| !s.is_empty())
.filter_map(|s| s.parse().ok())
.collect();
match numbers.as_slice() {
[detected, max, ..] => (*detected, *max),
[detected] => (*detected, 0),
_ => (0, 0),
}
}
+1
View File
@@ -1,6 +1,7 @@
[package]
name = "fal_client"
edition = "2021"
rust-version = "1.75" # MSRV: async traits
version = "0.0.1"
publish = false
@@ -0,0 +1,34 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
use reqwest::StatusCode;
#[derive(Debug)]
pub enum FalGenericApiError {
/// serde_json::Error, likely from JSON deserialization schema mismatch.
/// Includes the original body.
SerdeResponseParseErrorWithBody {
error: serde_json::Error,
body: String,
},
/// An uncategorized bad HTTP response.
UncategorizedBadResponseWithStatusAndBody {
status_code: StatusCode,
body: String,
},
}
impl Error for FalGenericApiError {}
impl Display for FalGenericApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::SerdeResponseParseErrorWithBody { error, body } => {
write!(f, "Failed to parse response body: {:?}. Body: {}", error, body)
}
Self::UncategorizedBadResponseWithStatusAndBody { status_code, body } => {
write!(f, "Uncategorized bad response: status code {}, body: {}", status_code, body)
}
}
}
}
@@ -0,0 +1,19 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum FalSpecificApiError {
/// The job is still in progress. The response URL is not ready yet.
/// Contains the detail message from the FAL API (e.g. "Request is still in progress").
IncompleteJob(String),
}
impl Error for FalSpecificApiError {}
impl Display for FalSpecificApiError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::IncompleteJob(detail) => write!(f, "Job is incomplete: {}", detail),
}
}
}
@@ -0,0 +1,18 @@
use std::error::Error;
use std::fmt::{Display, Formatter};
#[derive(Debug)]
pub enum FalClientError {
/// The URL provided was invalid or pointed to an unexpected host.
InvalidUrl(String),
}
impl Error for FalClientError {}
impl Display for FalClientError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidUrl(msg) => write!(f, "Invalid URL: {}", msg),
}
}
}

Some files were not shown because too many files have changed in this diff Show More