diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/common_quality.rs b/crates/api_clients/artcraft/artcraft_router/src/api/common_quality.rs new file mode 100644 index 0000000000..399a64d7be --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/api/common_quality.rs @@ -0,0 +1,11 @@ +use serde_derive::{Deserialize, Serialize}; + +/// Common quality levels you can specify when enqueuing a generation. +/// Not every model will use this; models that don't simply ignore it. +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum CommonQuality { + High, + Medium, + Low, +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs index c3d3386130..a383049879 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/api/mod.rs @@ -2,6 +2,7 @@ pub mod audio_list_ref; pub mod character_list_ref; pub mod common_aspect_ratio; pub mod common_image_model; +pub mod common_quality; pub mod common_resolution; pub mod common_splat_model; pub mod common_video_model; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_dev.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_dev.rs index d139f4080c..24679272d6 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_dev.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_dev.rs @@ -43,6 +43,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_schnell.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_schnell.rs index eb31099d13..af897ff131 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_schnell.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_1_schnell.rs @@ -31,6 +31,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(1), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_2_lora_angles.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_2_lora_angles.rs index 666ada8521..e21f2cc70f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_2_lora_angles.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_2_lora_angles.rs @@ -46,6 +46,7 @@ mod tests { image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), horizontal_angle: None, vertical_angle: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1.rs index cc561cec50..6580b346d2 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1.rs @@ -46,6 +46,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1_ultra.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1_ultra.rs index ce1c080e27..45045f625b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1_ultra.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_flux_pro_1p1_ultra.rs @@ -43,6 +43,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1.rs index ff022aaa9b..88f92ea03a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1.rs @@ -6,25 +6,40 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf pub(crate) fn estimate_image_cost_artcraft_gpt_image_1( plan: &PlanArtcraftGptImage1<'_>, ) -> ImageGenerationCostEstimate { - // What we charge in the legacy storyteller-web handlers (BYOK pricing - // approximation, kept for parity with `enqueue_gpt_image_1_byok_*` cost): - // Auto: 17¢/image (treated as High) - // Low: 1¢/image - // Medium: 4¢/image - // High: 17¢/image - // Cost is independent of image size on the artcraft (BYOK) tier. - let cost_per_image: u64 = match plan.quality { - ArtcraftGptImage1Quality::Auto => 17, - ArtcraftGptImage1Quality::Low => 1, - ArtcraftGptImage1Quality::Medium => 4, - ArtcraftGptImage1Quality::High => 17, + // Pricing per the fal-ai GPT Image 1 docs, adapted for our artcraft billing: + // + // Output image cost (per output image): + // Low – $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢ + // Medium – $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢ + // High – $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢ + // + // Input image cost (edit mode only, per input image, high-fidelity assumed): + // 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image + // + // Input text tokens: free (we don't charge for these). + // + let is_square = matches!( + plan.image_size, + None | Some(ArtcraftGptImage1Size::Square) + ); + + let output_cost_per_image: u64 = match (plan.quality, is_square) { + (ArtcraftGptImage1Quality::Auto, _) => 17, // Auto treated as High square + (ArtcraftGptImage1Quality::Low, true) => 2, + (ArtcraftGptImage1Quality::Low, false) => 2, + (ArtcraftGptImage1Quality::Medium, true) => 5, + (ArtcraftGptImage1Quality::Medium, false) => 7, + (ArtcraftGptImage1Quality::High, true) => 17, + (ArtcraftGptImage1Quality::High, false) => 25, }; - // Reference `image_size` so the field is exercised in coverage; pricing - // currently doesn't depend on it. - let _ = plan.image_size.unwrap_or(ArtcraftGptImage1Size::Square); + let num_output_images = plan.num_images.as_u64(); + let output_cost = output_cost_per_image * num_output_images; - let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64(); + // Input image token cost: 2¢ per input image (high-fidelity estimate). + let input_image_cost: u64 = 2 * plan.num_input_images; + + let cost_in_usd_cents = output_cost + input_image_cost; ImageGenerationCostEstimate { cost_in_credits: Some(cost_in_usd_cents), @@ -40,77 +55,185 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_1( mod tests { use crate::api::common_aspect_ratio::CommonAspectRatio; use crate::api::common_image_model::CommonImageModel; + use crate::api::common_quality::CommonQuality; + 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 tokens::tokens::media_files::MediaFileToken; - fn estimate_usd_cents(image_batch_count: u16, aspect_ratio: Option) -> u64 { - let request = GenerateImageRequest { + fn base() -> GenerateImageRequest<'static> { + GenerateImageRequest { model: CommonImageModel::GptImage1, provider: Provider::Artcraft, prompt: None, image_inputs: None, resolution: None, - aspect_ratio, - image_batch_count: Some(image_batch_count), + aspect_ratio: None, + quality: None, + image_batch_count: Some(1), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, idempotency_token: None, horizontal_angle: None, vertical_angle: None, zoom: None, - }; - request.build() - .expect("build should succeed") - .estimate_costs() - .cost_in_usd_cents - .expect("cost_in_usd_cents should be present") - } - - // The artcraft (BYOK) tier defaults plan.quality to High = 17¢/image and is - // independent of size. - - #[test] - fn default_quality_one_image_costs_17_cents() { - assert_eq!(estimate_usd_cents(1, None), 17); - } - - #[test] - fn default_quality_two_images_costs_34_cents() { - assert_eq!(estimate_usd_cents(2, None), 34); - } - - #[test] - fn default_quality_three_images_costs_51_cents() { - assert_eq!(estimate_usd_cents(3, None), 51); - } - - #[test] - fn default_quality_four_images_costs_68_cents() { - assert_eq!(estimate_usd_cents(4, None), 68); - } - - #[test] - fn cost_is_independent_of_aspect_ratio() { - // The BYOK tier doesn't price by image size — square, wide, tall, auto - // and unset all bill the same. - let ars = [ - None, - Some(CommonAspectRatio::Square), - Some(CommonAspectRatio::SquareHd), - Some(CommonAspectRatio::WideSixteenByNine), - Some(CommonAspectRatio::TallNineBySixteen), - Some(CommonAspectRatio::Auto), - Some(CommonAspectRatio::Auto2k), - Some(CommonAspectRatio::Auto4k), - ]; - for ar in ars { - assert_eq!( - estimate_usd_cents(1, ar), - 17, - "expected 17¢ regardless of aspect ratio (got {:?})", - ar, - ); } } + + fn estimate_text( + quality: Option, + aspect_ratio: Option, + batch: u16, + ) -> u64 { + let request = GenerateImageRequest { + quality, + aspect_ratio, + image_batch_count: Some(batch), + ..base() + }; + request.build().unwrap().estimate_costs() + .cost_in_usd_cents.unwrap() + } + + fn estimate_edit( + quality: Option, + aspect_ratio: Option, + batch: u16, + num_input_images: usize, + ) -> u64 { + let tokens: Vec = (0..num_input_images) + .map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i))) + .collect(); + let request = GenerateImageRequest { + quality, + aspect_ratio, + image_batch_count: Some(batch), + image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), + ..base() + }; + request.build().unwrap().estimate_costs() + .cost_in_usd_cents.unwrap() + } + + // ── Default quality (None → High) ─────────────────────────────────────── + + #[test] + fn default_quality_square_one_image_costs_17_cents() { + assert_eq!(estimate_text(None, None, 1), 17); + } + + #[test] + fn default_quality_square_four_images_costs_68_cents() { + assert_eq!(estimate_text(None, None, 4), 68); + } + + // ── Low quality (2¢/image regardless of size) ─────────────────────────── + + #[test] + fn low_square_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 2); + } + + #[test] + fn low_wide_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2); + } + + #[test] + fn low_tall_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2); + } + + #[test] + fn low_four_images() { + assert_eq!(estimate_text(Some(CommonQuality::Low), None, 4), 8); + } + + // ── Medium quality (5¢ square, 7¢ wide/tall) ─────────────────────────── + + #[test] + fn medium_square_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 5); + } + + #[test] + fn medium_wide_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 7); + } + + #[test] + fn medium_tall_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 7); + } + + #[test] + fn medium_square_four_images() { + assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 20); + } + + #[test] + fn medium_wide_four_images() { + assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 28); + } + + // ── High quality (17¢ square, 25¢ wide/tall) ─────────────────────────── + + #[test] + fn high_square_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17); + } + + #[test] + fn high_wide_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 25); + } + + #[test] + fn high_tall_one_image() { + assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 25); + } + + #[test] + fn high_square_four_images() { + assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 68); + } + + #[test] + fn high_wide_four_images() { + assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 100); + } + + // ── Edit mode: adds 2¢ per input image ───────────────────────────────── + + #[test] + fn edit_one_input_adds_2_cents() { + // High square 1 output = 17¢, plus 1 input × 2¢ = 19¢ + assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 1), 19); + } + + #[test] + fn edit_three_inputs_adds_6_cents() { + // High square 1 output = 17¢, plus 3 inputs × 2¢ = 23¢ + assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 3), 23); + } + + #[test] + fn edit_five_inputs_adds_10_cents() { + // Medium wide 2 outputs = 7×2 = 14¢, plus 5 inputs × 2¢ = 24¢ + assert_eq!(estimate_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 2, 5), 24); + } + + #[test] + fn edit_low_quality_with_inputs() { + // Low square 1 output = 2¢, plus 2 inputs × 2¢ = 6¢ + assert_eq!(estimate_edit(Some(CommonQuality::Low), None, 1, 2), 6); + } + + // ── Text mode (no input images) has no input cost ────────────────────── + + #[test] + fn text_mode_no_input_cost() { + assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17); + } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs index 5ff0af3d2e..eb80545784 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_gpt_image_1p5.rs @@ -82,6 +82,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs index 65bcb2ed81..db8af3e385 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana.rs @@ -43,6 +43,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs index ad3e00d2a9..4ba226b159 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_2.rs @@ -56,6 +56,7 @@ mod tests { image_inputs: None, resolution, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_pro.rs index a708e79b5b..3647630600 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_pro.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_nano_banana_pro.rs @@ -50,6 +50,7 @@ mod tests { image_inputs: None, resolution, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_qwen_edit_2511_angles.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_qwen_edit_2511_angles.rs index 4933a4132b..5015d6c577 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_qwen_edit_2511_angles.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_qwen_edit_2511_angles.rs @@ -46,6 +46,7 @@ mod tests { image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4.rs index b65a8ddc65..34031ac7a6 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4.rs @@ -43,6 +43,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4p5.rs index 111e5f8509..535f0327ca 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4p5.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_4p5.rs @@ -43,6 +43,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs index 08baf8b08b..9647024166 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/artcraft/estimate_image_cost_artcraft_seedream_5_lite.rs @@ -43,6 +43,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_gpt_image_1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_gpt_image_1.rs index ccc71afe4c..0fdc0cd1c8 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_gpt_image_1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_gpt_image_1.rs @@ -7,12 +7,19 @@ pub(crate) fn estimate_image_cost_fal_gpt_image_1( plan: &PlanFalGptImage1<'_>, ) -> ImageGenerationCostEstimate { // Per fal docs (fal-ai/gpt-image-1/{text-to-image,edit-image}): - // Low: $0.011 (1024x1024) / $0.016 (other) per image - // Medium: $0.042 (1024x1024) / $0.063 (other) per image - // High: $0.167 (1024x1024) / $0.250 (other) per image - // Round up to whole cents. + // + // Output image cost (per output image): + // Low: $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢ + // Medium: $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢ + // High: $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢ + // + // Input image tokens (edit mode, per input image, high-fidelity assumed): + // 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image + // + // Input text tokens: free (we don't charge for these). + // let is_square = matches!(plan.image_size, None | Some(FalGptImage1ImageSize::Square)); - let cost_per_image: u64 = match (plan.quality, is_square) { + let output_cost_per_image: u64 = match (plan.quality, is_square) { (FalGptImage1Quality::Low, true) => 2, (FalGptImage1Quality::Low, false) => 2, (FalGptImage1Quality::Medium, true) => 5, @@ -20,7 +27,18 @@ pub(crate) fn estimate_image_cost_fal_gpt_image_1( (FalGptImage1Quality::High, true) => 17, (FalGptImage1Quality::High, false) => 25, }; - let cost_in_usd_cents = cost_per_image * plan.num_images.as_u64(); + + let num_output_images = plan.num_images.as_u64(); + let output_cost = output_cost_per_image * num_output_images; + + // Input image token cost: 2¢ per input image (high-fidelity estimate). + let input_image_cost: u64 = if plan.image_urls.is_empty() { + 0 + } else { + 2 * plan.image_urls.len() as u64 + }; + + let cost_in_usd_cents = output_cost + input_image_cost; ImageGenerationCostEstimate { cost_in_credits: Some(cost_in_usd_cents), @@ -37,7 +55,7 @@ mod tests { use super::*; use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::FalGptImage1NumImages; - fn plan( + fn text_plan( quality: FalGptImage1Quality, image_size: Option, num_images: FalGptImage1NumImages, @@ -51,113 +69,170 @@ mod tests { } } - fn cents( + fn edit_plan( + quality: FalGptImage1Quality, + image_size: Option, + num_images: FalGptImage1NumImages, + num_input_images: usize, + ) -> PlanFalGptImage1<'static> { + let image_urls = (0..num_input_images) + .map(|i| format!("https://fake.example.com/img{}.png", i)) + .collect(); + PlanFalGptImage1 { + prompt: Some("p"), + image_urls, + image_size, + quality, + num_images, + } + } + + fn cents_text( quality: FalGptImage1Quality, image_size: Option, num_images: FalGptImage1NumImages, ) -> u64 { - estimate_image_cost_fal_gpt_image_1(&plan(quality, image_size, num_images)) + estimate_image_cost_fal_gpt_image_1(&text_plan(quality, image_size, num_images)) .cost_in_usd_cents - .expect("cost_in_usd_cents should be present") + .unwrap() } - // ── Low quality (2¢/image, both square and non-square) ──────────────────── + fn cents_edit( + quality: FalGptImage1Quality, + image_size: Option, + num_images: FalGptImage1NumImages, + num_input_images: usize, + ) -> u64 { + estimate_image_cost_fal_gpt_image_1(&edit_plan(quality, image_size, num_images, num_input_images)) + .cost_in_usd_cents + .unwrap() + } + + // ── Low quality text-to-image (2¢/image, both sizes) ────────────────── #[test] - fn low_square_one_image_costs_2_cents() { - assert_eq!(cents(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 2); + fn low_square_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 2); } #[test] - fn low_horizontal_one_image_costs_2_cents() { - assert_eq!(cents(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 2); + fn low_horizontal_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 2); } #[test] - fn low_vertical_one_image_costs_2_cents() { - assert_eq!(cents(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 2); + fn low_vertical_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 2); } #[test] - fn low_unset_size_costs_2_cents() { - // Unset image_size is treated as square in pricing. - assert_eq!(cents(FalGptImage1Quality::Low, None, FalGptImage1NumImages::One), 2); + fn low_unset_size_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::One), 2); } #[test] - fn low_four_images_costs_8_cents() { - assert_eq!(cents(FalGptImage1Quality::Low, None, FalGptImage1NumImages::Four), 8); + fn low_four_images() { + assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::Four), 8); } - // ── Medium quality (5¢ square, 7¢ wide/tall) ────────────────────────────── + // ── Medium quality text-to-image (5¢ square, 7¢ wide/tall) ──────────── #[test] - fn medium_square_one_image_costs_5_cents() { - assert_eq!(cents(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 5); + fn medium_square_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 5); } #[test] - fn medium_horizontal_one_image_costs_7_cents() { - assert_eq!(cents(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 7); + fn medium_horizontal_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 7); } #[test] - fn medium_vertical_one_image_costs_7_cents() { - assert_eq!(cents(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 7); + fn medium_vertical_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 7); } #[test] - fn medium_unset_size_costs_5_cents() { - assert_eq!(cents(FalGptImage1Quality::Medium, None, FalGptImage1NumImages::One), 5); + fn medium_square_four_images() { + assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Four), 20); } #[test] - fn medium_square_four_images_costs_20_cents() { - assert_eq!(cents(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Four), 20); + fn medium_horizontal_four_images() { + assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 28); + } + + // ── High quality text-to-image (17¢ square, 25¢ wide/tall) ──────────── + + #[test] + fn high_square_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17); } #[test] - fn medium_horizontal_four_images_costs_28_cents() { - assert_eq!(cents(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 28); - } - - // ── High quality (17¢ square, 25¢ wide/tall) ────────────────────────────── - - #[test] - fn high_square_one_image_costs_17_cents() { - assert_eq!(cents(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17); + fn high_horizontal_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 25); } #[test] - fn high_horizontal_one_image_costs_25_cents() { - assert_eq!(cents(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 25); + fn high_vertical_one_image() { + assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 25); } #[test] - fn high_vertical_one_image_costs_25_cents() { - assert_eq!(cents(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 25); + fn high_square_two_images() { + assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Two), 34); } #[test] - fn high_unset_size_costs_17_cents() { - assert_eq!(cents(FalGptImage1Quality::High, None, FalGptImage1NumImages::One), 17); + fn high_horizontal_four_images() { + assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 100); + } + + // ── Edit mode adds 2¢ per input image ───────────────────────────────── + + #[test] + fn edit_high_square_one_output_one_input() { + // 17¢ output + 2¢ input = 19¢ + assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 1), 19); } #[test] - fn high_square_two_images_costs_34_cents() { - assert_eq!(cents(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Two), 34); + fn edit_high_square_one_output_three_inputs() { + // 17¢ output + 3×2¢ input = 23¢ + assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 3), 23); } #[test] - fn high_horizontal_four_images_costs_100_cents() { - assert_eq!(cents(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 100); + fn edit_medium_wide_two_outputs_five_inputs() { + // 2×7¢ output + 5×2¢ input = 24¢ + assert_eq!(cents_edit(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Two, 5), 24); } - // ── Metadata flags ──────────────────────────────────────────────────────── + #[test] + fn edit_low_square_one_output_two_inputs() { + // 2¢ output + 2×2¢ input = 6¢ + assert_eq!(cents_edit(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 2), 6); + } + + #[test] + fn edit_high_vertical_four_outputs_one_input() { + // 4×25¢ output + 1×2¢ input = 102¢ + assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::Four, 1), 102); + } + + // ── Text mode has no input image cost ───────────────────────────────── + + #[test] + fn text_mode_no_input_cost() { + assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17); + } + + // ── Metadata flags ──────────────────────────────────────────────────── #[test] fn metadata_flags_are_default() { - let estimate = estimate_image_cost_fal_gpt_image_1(&plan( + let estimate = estimate_image_cost_fal_gpt_image_1(&text_plan( FalGptImage1Quality::Medium, None, FalGptImage1NumImages::One, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_nano_banana_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_nano_banana_pro.rs index fe072f47b7..17643594b5 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_nano_banana_pro.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/cost/fal/estimate_image_cost_fal_nano_banana_pro.rs @@ -48,6 +48,7 @@ mod tests { image_inputs: None, resolution, aspect_ratio: None, + quality: None, image_batch_count: Some(image_batch_count), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_dev.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_dev.rs index bc0a297a96..538fd74101 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_dev.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_dev.rs @@ -47,6 +47,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_flux_1_dev_image_request() @@ -69,6 +70,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), + quality: None, image_batch_count: Some(4), prompt: Some("a dog surfing a wave, cinematic"), ..base_flux_1_dev_image_request() @@ -94,7 +96,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a bear walking through an abandoned desert city at night"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_flux_1_dev_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_schnell.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_schnell.rs index 6880978b4d..dcd3bd4c11 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_schnell.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_1_schnell.rs @@ -47,6 +47,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a man and woman walking through a cyberpunk city at night"), ..base_flux_1_schnell_image_request() @@ -69,6 +70,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), + quality: None, image_batch_count: Some(4), prompt: Some("a dog surfing a wave, cinematic"), ..base_flux_1_schnell_image_request() @@ -94,7 +96,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a man and woman walking through a futuristic city at evening"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_flux_1_schnell_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_2_lora_angles.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_2_lora_angles.rs index 198d8bb303..33deac71ed 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_2_lora_angles.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_2_lora_angles.rs @@ -51,6 +51,7 @@ mod tests { let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")]; let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::SquareHd), + quality: None, image_batch_count: Some(1), image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), horizontal_angle: Some(30.0), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1.rs index e481a63d09..3704f97f2c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1.rs @@ -47,6 +47,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_flux_pro_1p1_image_request() @@ -69,6 +70,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), + quality: None, image_batch_count: Some(4), prompt: Some("a dog surfing a wave, cinematic"), ..base_flux_pro_1p1_image_request() @@ -94,7 +96,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a bear walking through an abandoned desert city at night"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_flux_pro_1p1_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1_ultra.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1_ultra.rs index 0ccfec47a6..c389c7fc92 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1_ultra.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_flux_pro_1p1_ultra.rs @@ -47,6 +47,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_flux_pro_1p1_ultra_image_request() @@ -69,6 +70,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), + quality: None, image_batch_count: Some(4), prompt: Some("a dog surfing a wave, cinematic"), ..base_flux_pro_1p1_ultra_image_request() @@ -94,7 +96,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a t-rex walking through an abandoned desert city at night"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_flux_pro_1p1_ultra_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs index 4094986089..72f95e0a96 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana.rs @@ -50,6 +50,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_nano_banana_image_request() @@ -79,6 +80,7 @@ mod tests { prompt: Some("Change the background to a sunny beach"), image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(CommonAspectRatio::Auto), + quality: None, image_batch_count: Some(1), ..base_nano_banana_image_request() }; @@ -103,7 +105,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a shiba walking through a cyberpunk city at night"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_nano_banana_image_request() @@ -138,7 +141,8 @@ mod tests { prompt: Some("Change the background to a sunny beach"), image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_nano_banana_image_request() }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs index fd89850be3..ea810157ef 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_2.rs @@ -49,6 +49,7 @@ mod tests { let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), resolution: Some(CommonResolution::OneK), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_nano_banana_2_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_pro.rs index f446595290..f4ecb83c44 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_pro.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_nano_banana_pro.rs @@ -51,6 +51,7 @@ mod tests { let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), resolution: Some(CommonResolution::OneK), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_image_request() @@ -74,6 +75,7 @@ mod tests { let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), resolution: Some(CommonResolution::OneK), + quality: None, image_batch_count: Some(4), prompt: Some("a dog surfing a wave, cinematic"), ..base_image_request() @@ -106,6 +108,7 @@ mod tests { image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions resolution: Some(CommonResolution::OneK), + quality: None, image_batch_count: Some(1), ..base_image_request() }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_qwen_edit_2511_angles.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_qwen_edit_2511_angles.rs index d48d06833c..4845b1c416 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_qwen_edit_2511_angles.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_qwen_edit_2511_angles.rs @@ -52,6 +52,7 @@ mod tests { let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")]; let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::SquareHd), + quality: None, image_batch_count: Some(1), prompt: None, image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4.rs index 379d19cf4b..809b2198b3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4.rs @@ -51,6 +51,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a cat walking through a cyberpunk city at night"), ..base_seedream_4_image_request() @@ -80,6 +81,7 @@ mod tests { prompt: Some("Change the background to a sunny beach"), image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(CommonAspectRatio::Auto), + quality: None, image_batch_count: Some(1), ..base_seedream_4_image_request() }; @@ -104,7 +106,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a shiba walking through a cyberpunk city at night"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_seedream_4_image_request() @@ -139,7 +142,8 @@ mod tests { prompt: Some("Change the background to a sunny beach"), image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_seedream_4_image_request() }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4p5.rs index 7b4bca8e19..589a5d62e3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4p5.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_4p5.rs @@ -51,6 +51,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a corgi walking through a cyberpunk city at night"), ..base_seedream_4p5_image_request() @@ -80,6 +81,7 @@ mod tests { prompt: Some("Change the background to a spooky haunted house"), image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(CommonAspectRatio::Auto2k), + quality: None, image_batch_count: Some(1), ..base_seedream_4p5_image_request() }; @@ -104,7 +106,8 @@ mod tests { println!("--- text-to-image aspect ratio: {:?} ---", ar); let request = GenerateImageRequest { aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), prompt: Some("a corgi walking through a cyberpunk city at night"), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_seedream_4p5_image_request() @@ -139,7 +142,8 @@ mod tests { prompt: Some("Change the background to a spooky haunted house"), image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)), aspect_ratio: Some(ar), - image_batch_count: Some(1), + quality: None, + image_batch_count: Some(1), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, ..base_seedream_4p5_image_request() }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs index 4d2384e730..2297d50216 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/artcraft/generate_image_artcraft_seedream_5_lite.rs @@ -47,6 +47,7 @@ mod tests { let client = get_artcraft_client(); let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), + quality: None, image_batch_count: Some(1), prompt: Some("a corgi walking through a cyberpunk city at night"), ..base_seedream_5_lite_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_gpt_image_1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_gpt_image_1.rs index 0e010fbb1e..af95c806d5 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_gpt_image_1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_gpt_image_1.rs @@ -75,6 +75,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_nano_banana_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_nano_banana_pro.rs index a3d7c8bb57..e59d80c615 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_nano_banana_pro.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/execute/fal/generate_image_fal_nano_banana_pro.rs @@ -105,6 +105,7 @@ mod tests { let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine), resolution: Some(CommonResolution::TwoK), + quality: None, image_batch_count: Some(1), prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets"), ..base_fal_image_request() @@ -129,6 +130,7 @@ mod tests { let request = GenerateImageRequest { aspect_ratio: Some(CommonAspectRatio::Square), resolution: Some(CommonResolution::OneK), + quality: None, image_batch_count: Some(4), prompt: Some("a golden retriever surfing a wave, cinematic, 4K"), ..base_fal_image_request() @@ -161,6 +163,7 @@ mod tests { image_inputs: Some(ImageListRef::Urls(&image_urls)), aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions resolution: Some(CommonResolution::TwoK), + quality: None, image_batch_count: Some(1), ..base_fal_image_request() }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs index 75d626a589..96dc075a64 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request.rs @@ -1,5 +1,6 @@ use crate::api::common_aspect_ratio::CommonAspectRatio; use crate::api::common_image_model::CommonImageModel; +use crate::api::common_quality::CommonQuality; use crate::api::common_resolution::CommonResolution; use crate::api::image_list_ref::ImageListRef; use crate::api::provider::Provider; @@ -57,6 +58,9 @@ pub struct GenerateImageRequest<'a> { /// The aspect ratio to use. pub aspect_ratio: Option, + /// The quality level for generation. Not all models use this. + pub quality: Option, + /// How many images to generate. pub image_batch_count: Option, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_dev.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_dev.rs index 93b4014cfe..6bb0f438b6 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_dev.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_dev.rs @@ -268,6 +268,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_flux_1_dev_image_request() @@ -295,6 +296,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_flux_1_dev_image_request() @@ -312,6 +314,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_flux_1_dev_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_schnell.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_schnell.rs index 93ae509362..c0a2bf4451 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_schnell.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_1_schnell.rs @@ -264,6 +264,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_flux_1_schnell_image_request() @@ -291,6 +292,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_flux_1_schnell_image_request() @@ -308,6 +310,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_flux_1_schnell_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_2_lora_angles.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_2_lora_angles.rs index 03a8d42a9c..c2b52d5752 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_2_lora_angles.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_2_lora_angles.rs @@ -224,6 +224,7 @@ mod tests { let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)]; for (count, expected) in cases { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(count), image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), ..base_flux_2_lora_angles_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1.rs index 94441264a6..a978a8ef0a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1.rs @@ -268,6 +268,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_flux_pro_1p1_image_request() @@ -295,6 +296,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_flux_pro_1p1_image_request() @@ -312,6 +314,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_flux_pro_1p1_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1_ultra.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1_ultra.rs index 512d85482f..c42984bb63 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1_ultra.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_flux_pro_1p1_ultra.rs @@ -271,6 +271,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_flux_pro_1p1_ultra_image_request() @@ -298,6 +299,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_flux_pro_1p1_ultra_image_request() @@ -315,6 +317,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_flux_pro_1p1_ultra_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1.rs index 6b8786c330..ab7e2f3aef 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_gpt_image_1.rs @@ -1,4 +1,5 @@ use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_quality::CommonQuality; use crate::api::image_list_ref::ImageListRef; use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use crate::errors::artcraft_router_error::ArtcraftRouterError; @@ -46,6 +47,10 @@ pub struct PlanArtcraftGptImage1<'a> { pub prompt: Option<&'a str>, /// Input images for image editing. None means text-to-image mode. pub image_inputs: Option<&'a Vec>, + /// Number of input images (0 for text-to-image). This is counted from either + /// media tokens or hydrated URLs, so the cost estimator can use it without + /// needing to inspect image_inputs (which is None for URL-form inputs). + pub num_input_images: u64, pub image_size: Option, /// Quality defaults to High when the request leaves it unspecified, matching /// the legacy storyteller-web handler defaults. @@ -60,15 +65,17 @@ pub fn plan_generate_image_artcraft_gpt_image_1<'a>( let strategy = request.request_mismatch_mitigation_strategy; let image_inputs = resolve_image_list_ref(request.image_inputs)?; + let num_input_images = count_input_images(request.image_inputs); let image_size = plan_image_size(request.aspect_ratio); + let quality = plan_quality(request.quality); let num_images = plan_num_images(request.image_batch_count, strategy)?; Ok(ImageGenerationPlan::ArtcraftGptImage1(PlanArtcraftGptImage1 { prompt: request.prompt, image_inputs, + num_input_images, image_size, - // Match the legacy handler default of High for both modes. - quality: ArtcraftGptImage1Quality::High, + quality, num_images, idempotency_token: request.get_or_generate_idempotency_token(), })) @@ -88,6 +95,23 @@ fn resolve_image_list_ref<'a>( } } +fn count_input_images(image_inputs: Option>) -> u64 { + match image_inputs { + None => 0, + Some(ImageListRef::MediaFileTokens(tokens)) => tokens.len() as u64, + Some(ImageListRef::Urls(urls)) => urls.len() as u64, + } +} + +fn plan_quality(quality: Option) -> ArtcraftGptImage1Quality { + match quality { + Some(CommonQuality::Low) => ArtcraftGptImage1Quality::Low, + Some(CommonQuality::Medium) => ArtcraftGptImage1Quality::Medium, + Some(CommonQuality::High) => ArtcraftGptImage1Quality::High, + None => ArtcraftGptImage1Quality::High, // Legacy handler default + } +} + // GPT Image 1 image sizes: Square (1024x1024), Horizontal (1536x1024), // Vertical (1024x1536). All 17 CommonAspectRatio variants map to one of these // or None (let the legacy handler default to Square). diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs index 9ba5cc4f2a..a14f409135 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana.rs @@ -294,6 +294,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_nano_banana_image_request() @@ -327,6 +328,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_nano_banana_image_request() @@ -345,6 +347,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_nano_banana_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_pro.rs index ea6736a66c..5acf1b229d 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_pro.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_nano_banana_pro.rs @@ -357,6 +357,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_image_request() @@ -390,6 +391,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_image_request() @@ -408,6 +410,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_qwen_edit_2511_angles.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_qwen_edit_2511_angles.rs index 2b182f736b..5dad342099 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_qwen_edit_2511_angles.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_qwen_edit_2511_angles.rs @@ -226,6 +226,7 @@ mod tests { let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)]; for (count, expected) in cases { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(count), image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)), ..base_qwen_edit_2511_angles_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs index 12f39a671e..ebde9c7339 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4.rs @@ -295,6 +295,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_seedream_4_image_request() @@ -322,6 +323,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_seedream_4_image_request() @@ -336,6 +338,7 @@ mod tests { fn num_images_out_of_range_clamps_to_four() { for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_seedream_4_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4p5.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4p5.rs index ba334203e3..41b14e4a31 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4p5.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/artcraft/plan_generate_image_artcraft_seedream_4p5.rs @@ -296,6 +296,7 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_seedream_4p5_image_request() @@ -323,6 +324,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_seedream_4p5_image_request() @@ -337,6 +339,7 @@ mod tests { fn num_images_out_of_range_clamps_to_four() { for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_seedream_4p5_image_request() diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_gpt_image_1.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_gpt_image_1.rs index 9895327782..456dd2b0b9 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_gpt_image_1.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_gpt_image_1.rs @@ -1,4 +1,5 @@ use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_quality::CommonQuality; use crate::api::image_list_ref::ImageListRef; use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; use crate::errors::artcraft_router_error::ArtcraftRouterError; @@ -56,13 +57,14 @@ pub fn plan_generate_image_fal_gpt_image_1<'a>( let strategy = request.request_mismatch_mitigation_strategy; let image_urls = resolve_image_list_ref(request.image_inputs)?; let image_size = plan_image_size(request.aspect_ratio); + let quality = plan_quality(request.quality); let num_images = plan_num_images(request.image_batch_count, strategy)?; Ok(ImageGenerationPlan::FalGptImage1(PlanFalGptImage1 { prompt: request.prompt, image_urls, image_size, - quality: FalGptImage1Quality::Medium, + quality, num_images, })) } @@ -79,6 +81,15 @@ fn resolve_image_list_ref( } } +fn plan_quality(quality: Option) -> FalGptImage1Quality { + match quality { + Some(CommonQuality::Low) => FalGptImage1Quality::Low, + Some(CommonQuality::Medium) => FalGptImage1Quality::Medium, + Some(CommonQuality::High) => FalGptImage1Quality::High, + None => FalGptImage1Quality::Medium, // Fal API default + } +} + fn plan_image_size(aspect_ratio: Option) -> Option { use FalGptImage1ImageSize as S; match aspect_ratio { @@ -215,6 +226,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_nano_banana_pro.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_nano_banana_pro.rs index f286d709f3..989932314b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_nano_banana_pro.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/plan/fal/plan_generate_image_fal_nano_banana_pro.rs @@ -256,6 +256,7 @@ mod tests { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -478,7 +479,8 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { - image_batch_count: Some(0), + quality: None, + image_batch_count: Some(0), request_mismatch_mitigation_strategy: strategy, ..base_fal_request() }; @@ -511,6 +513,7 @@ mod tests { #[test] fn num_images_out_of_range_error_out() { let request = GenerateImageRequest { + quality: None, image_batch_count: Some(5), request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, ..base_fal_request() @@ -529,7 +532,8 @@ mod tests { RequestMismatchMitigationStrategy::PayLessDowngrade, ] { let request = GenerateImageRequest { - image_batch_count: Some(5), + quality: None, + image_batch_count: Some(5), request_mismatch_mitigation_strategy: strategy, ..base_fal_request() }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs index d021110598..2f003763e4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/test_helpers.rs @@ -29,6 +29,7 @@ pub fn base_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -55,6 +56,7 @@ pub fn base_fal_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -73,6 +75,7 @@ pub fn base_seedream_4_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -91,6 +94,7 @@ pub fn base_seedream_4p5_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -109,6 +113,7 @@ pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -127,6 +132,7 @@ pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -145,6 +151,7 @@ pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -163,6 +170,7 @@ pub fn base_gpt_image_1_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -181,6 +189,7 @@ pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -199,6 +208,7 @@ pub fn base_flux_1_dev_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -217,6 +227,7 @@ pub fn base_flux_1_schnell_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -235,6 +246,7 @@ pub fn base_flux_pro_1p1_image_request() -> GenerateImageRequest<'static> { image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -253,6 +265,7 @@ pub fn base_qwen_edit_2511_angles_image_request() -> GenerateImageRequest<'stati image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -271,6 +284,7 @@ pub fn base_flux_2_lora_angles_image_request() -> GenerateImageRequest<'static> image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, @@ -289,6 +303,7 @@ pub fn base_flux_pro_1p1_ultra_image_request() -> GenerateImageRequest<'static> image_inputs: None, resolution: None, aspect_ratio: None, + quality: None, image_batch_count: None, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, generation_mode_mismatch_strategy: None, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs index 81024db4bc..e8e79f8454 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs @@ -45,6 +45,7 @@ pub async fn estimate_image_cost_handler( image_inputs: None, // TODO: Only some models charge for this - we'll need to add later. resolution: router_resolution, aspect_ratio: router_aspect_ratio, + quality: None, image_batch_count: request.image_batch_count, request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade, generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway), diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_helper/hydrate_to_router_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_helper/hydrate_to_router_request.rs index 723b77723a..af62a87f18 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_helper/hydrate_to_router_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_helper/hydrate_to_router_request.rs @@ -1,6 +1,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioRouter; use artcraft_router::api::common_image_model::CommonImageModel as CommonImageModelRouter; +use artcraft_router::api::common_quality::CommonQuality as CommonQualityRouter; use artcraft_router::api::common_resolution::CommonResolution as CommonResolutionRouter; use artcraft_router::api::image_list_ref::ImageListRef; use artcraft_router::api::provider::Provider; @@ -8,6 +9,7 @@ use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismat use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest; use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum; use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum; +use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum; use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum; use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError; @@ -45,6 +47,17 @@ fn convert_resolution( }) } +fn convert_quality( + quality: &CommonQualityEnum, +) -> Result { + let json = serde_json::to_string(quality)?; + serde_json::from_str(&json).map_err(|e| { + AdvancedCommonWebError::BadInputWithSimpleMessage( + format!("Unsupported quality: {}", e), + ) + }) +} + pub fn hydrate_to_router_request( request: &OmniGenImageCostAndGenerateRequest, ) -> Result, AdvancedCommonWebError> { @@ -63,6 +76,10 @@ pub fn hydrate_to_router_request( .map(convert_resolution) .transpose()?; + let quality = request.quality.as_ref() + .map(convert_quality) + .transpose()?; + Ok(GenerateImageRequest { model, provider: Provider::Artcraft, @@ -71,6 +88,7 @@ pub fn hydrate_to_router_request( .map(ImageListRef::MediaFileTokens), resolution, aspect_ratio, + quality, image_batch_count: request.image_batch_count, horizontal_angle: request.horizontal_angle, vertical_angle: request.vertical_angle, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_image_request.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_image_request.rs index 641968d512..e88c6f8b71 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_image_request.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/distill_image_request.rs @@ -144,6 +144,7 @@ pub fn distill_image_request( image_inputs: image_inputs_static, resolution: initial.resolution, aspect_ratio: initial.aspect_ratio, + quality: initial.quality, image_batch_count: initial.image_batch_count, horizontal_angle: initial.horizontal_angle, vertical_angle: initial.vertical_angle, diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_gpt_image_1.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_gpt_image_1.rs index 8b5679287c..ef0d8c5ed7 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_gpt_image_1.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/tests/test_gpt_image_1.rs @@ -4,14 +4,21 @@ //! The omni-gen distillation routes through Fal for the execution plan and //! Artcraft for billing. //! -//! Artcraft-tier pricing (the rate the user is billed) matches the legacy -//! BYOK handlers (`generate_gpt_image_1_text_to_image_handler` / -//! `gpt_image_1_edit_image_handler`). The artcraft plan defaults quality = -//! High, which is 17¢/image regardless of image size. +//! Artcraft-tier pricing (the rate the user is billed): //! -//! The Fal execution plan defaults quality = Medium and maps aspect ratios to -//! three image sizes: Square (1024×1024), Horizontal (1536×1024), Vertical -//! (1024×1536). Auto/unset maps to None (API default). +//! Output image cost (per output image): +//! Low – 2¢ (square/unset) / 2¢ (wide or tall) +//! Medium – 5¢ (square/unset) / 7¢ (wide or tall) +//! High – 17¢ (square/unset) / 25¢ (wide or tall) +//! None → defaults to High +//! +//! Input image cost (edit mode only, per input image): +//! 2¢ per input image (high-fidelity token estimate) +//! +//! The Fal execution plan maps quality from the omni request (defaulting to +//! Medium when unspecified) and maps aspect ratios to three image sizes: +//! Square (1024×1024), Horizontal (1536×1024), Vertical (1024×1536). +//! Auto/unset maps to None (API default). #[cfg(test)] mod tests { @@ -20,6 +27,7 @@ mod tests { use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; use enums::common::generation::common_aspect_ratio::CommonAspectRatio; use enums::common::generation::common_image_model::CommonImageModel; + use enums::common::generation::common_quality::CommonQuality; use tokens::tokens::media_files::MediaFileToken; use url::Url; @@ -32,6 +40,7 @@ mod tests { fn make_request( prompt: Option<&str>, aspect_ratio: Option, + quality: Option, image_batch_count: Option, image_media_tokens: Option>, ) -> OmniGenImageCostAndGenerateRequest { @@ -42,7 +51,7 @@ mod tests { image_media_tokens, resolution: None, aspect_ratio, - quality: None, + quality, image_batch_count, horizontal_angle: None, vertical_angle: None, @@ -79,240 +88,351 @@ mod tests { // ──────────────────────────────────────────────────────────────────────────── // COST // ──────────────────────────────────────────────────────────────────────────── - // - // Pricing comes from `estimate_image_cost_artcraft_gpt_image_1`: - // quality = High (hardcoded default) → 17¢/image, independent of size. - // - // Same rate applies to both text-to-image and edit modes. mod cost { use super::*; mod text { use super::*; - fn cost_for_batch(image_batch_count: Option) -> (Option, Option) { - let request = make_request(Some("a happy puppy"), None, image_batch_count, None); - let distilled = distill_text(&request); - ( - distilled.cost.cost_in_credits, - distilled.cost.cost_in_usd_cents, - ) + fn cost( + quality: Option, + aspect_ratio: Option, + batch: Option, + ) -> u64 { + let request = make_request(Some("p"), aspect_ratio, quality, batch, None); + distill_text(&request).cost.cost_in_usd_cents.unwrap() + } + + // ── Default quality (None → High) ──────────────────────────────────── + + #[test] + fn default_quality_square_costs_17() { + assert_eq!(cost(None, Some(CommonAspectRatio::Square), Some(1)), 17); } #[test] - fn default_batch_costs_17_cents() { - let (credits, cents) = cost_for_batch(None); - assert_eq!(credits, Some(17)); - assert_eq!(cents, Some(17)); + fn default_quality_wide_costs_25() { + assert_eq!(cost(None, Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 25); } #[test] - fn batch_of_one_costs_17_cents() { - let (_c, cents) = cost_for_batch(Some(1)); - assert_eq!(cents, Some(17)); + fn default_quality_tall_costs_25() { + assert_eq!(cost(None, Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 25); } #[test] - fn batch_of_two_costs_34_cents() { - let (_c, cents) = cost_for_batch(Some(2)); - assert_eq!(cents, Some(34)); + fn default_quality_unset_size_costs_17() { + // Unset aspect_ratio → None image_size → treated as square = 17¢ + assert_eq!(cost(None, None, Some(1)), 17); } #[test] - fn batch_of_three_costs_51_cents() { - let (_c, cents) = cost_for_batch(Some(3)); - assert_eq!(cents, Some(51)); + fn default_quality_auto_costs_17() { + assert_eq!(cost(None, Some(CommonAspectRatio::Auto), Some(1)), 17); + } + + // ── Low quality (2¢/image regardless of size) ──────────────────────── + + #[test] + fn low_square_costs_2() { + assert_eq!(cost(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), Some(1)), 2); } #[test] - fn batch_of_four_costs_68_cents() { - let (_c, cents) = cost_for_batch(Some(4)); - assert_eq!(cents, Some(68)); + fn low_wide_costs_2() { + assert_eq!(cost(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 2); } #[test] - fn batch_above_max_clamps_and_costs_68_cents() { - // PayMoreUpgrade clamps over-large batches to 4. - let (_c, cents) = cost_for_batch(Some(7)); - assert_eq!(cents, Some(68)); + fn low_tall_costs_2() { + assert_eq!(cost(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 2); } #[test] - fn cost_is_independent_of_aspect_ratio() { - let aspect_ratios = [ - None, - Some(CommonAspectRatio::Square), - Some(CommonAspectRatio::SquareHd), - Some(CommonAspectRatio::WideSixteenByNine), - Some(CommonAspectRatio::WideTwentyOneByNine), - Some(CommonAspectRatio::TallNineBySixteen), - Some(CommonAspectRatio::TallNineByTwentyOne), - Some(CommonAspectRatio::Auto), - Some(CommonAspectRatio::Auto2k), - ]; - for ar in aspect_ratios { - let request = make_request(Some("p"), ar, Some(2), None); - let distilled = distill_text(&request); - assert_eq!( - distilled.cost.cost_in_usd_cents, - Some(34), - "expected 34¢ regardless of aspect ratio (got {:?})", - ar, - ); - } + fn low_unset_costs_2() { + assert_eq!(cost(Some(CommonQuality::Low), None, Some(1)), 2); } #[test] - fn cost_is_independent_of_prompt() { - let with_prompt = distill_text(&make_request(Some("a cat"), None, Some(3), None)); - let without_prompt = distill_text(&make_request(None, None, Some(3), None)); - assert_eq!(with_prompt.cost.cost_in_usd_cents, Some(51)); - assert_eq!(without_prompt.cost.cost_in_usd_cents, Some(51)); + fn low_four_images_costs_8() { + assert_eq!(cost(Some(CommonQuality::Low), None, Some(4)), 8); } + #[test] + fn low_batch_above_max_clamps_to_8() { + assert_eq!(cost(Some(CommonQuality::Low), None, Some(7)), 8); + } + + // ── Medium quality (5¢ square, 7¢ wide/tall) ───────────────────────── + + #[test] + fn medium_square_costs_5() { + assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(1)), 5); + } + + #[test] + fn medium_wide_costs_7() { + assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 7); + } + + #[test] + fn medium_tall_costs_7() { + assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 7); + } + + #[test] + fn medium_unset_costs_5() { + assert_eq!(cost(Some(CommonQuality::Medium), None, Some(1)), 5); + } + + #[test] + fn medium_square_four_images_costs_20() { + assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(4)), 20); + } + + #[test] + fn medium_wide_four_images_costs_28() { + assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(4)), 28); + } + + // ── High quality (17¢ square, 25¢ wide/tall) ───────────────────────── + + #[test] + fn high_square_costs_17() { + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1)), 17); + } + + #[test] + fn high_wide_costs_25() { + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 25); + } + + #[test] + fn high_tall_costs_25() { + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 25); + } + + #[test] + fn high_unset_costs_17() { + assert_eq!(cost(Some(CommonQuality::High), None, Some(1)), 17); + } + + #[test] + fn high_square_four_images_costs_68() { + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(4)), 68); + } + + #[test] + fn high_wide_four_images_costs_100() { + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(4)), 100); + } + + #[test] + fn high_batch_above_max_clamps_to_four() { + assert_eq!(cost(Some(CommonQuality::High), None, Some(9)), 68); + } + + // ── Batch scaling ──────────────────────────────────────────────────── + + #[test] + fn high_square_batch_scaling() { + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1)), 17); + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(2)), 34); + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(3)), 51); + assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(4)), 68); + } + + // ── Metadata flags ─────────────────────────────────────────────────── + #[test] fn cost_metadata_flags_are_default() { - let request = make_request(Some("p"), None, Some(1), None); + let request = make_request(Some("p"), None, None, Some(1), None); let distilled = distill_text(&request); assert!(!distilled.cost.is_free); assert!(!distilled.cost.is_unlimited); assert!(!distilled.cost.is_rate_limited); assert!(!distilled.cost.has_watermark); } + + // ── Cost is independent of prompt ───────────────────────────────────── + + #[test] + fn cost_is_independent_of_prompt() { + let with_prompt = distill_text(&make_request(Some("a cat"), None, None, Some(1), None)); + let without_prompt = distill_text(&make_request(None, None, None, Some(1), None)); + assert_eq!(with_prompt.cost.cost_in_usd_cents, without_prompt.cost.cost_in_usd_cents); + } } mod edit { use super::*; - fn cost_for_batch_with_refs( - image_batch_count: Option, - num_image_refs: usize, - ) -> (Option, Option) { - let (tokens, hydration) = fake_image_refs(num_image_refs); - let request = make_request(Some("make it pop"), None, image_batch_count, Some(tokens)); - let distilled = distill_edit(&request, &hydration); - ( - distilled.cost.cost_in_credits, - distilled.cost.cost_in_usd_cents, - ) + fn cost_edit( + quality: Option, + aspect_ratio: Option, + batch: Option, + num_input_images: usize, + ) -> u64 { + let (tokens, hydration) = fake_image_refs(num_input_images); + let request = make_request(Some("p"), aspect_ratio, quality, batch, Some(tokens)); + distill_edit(&request, &hydration).cost.cost_in_usd_cents.unwrap() + } + + // ── Input image cost adds 2¢ per input ────────────────────────────── + + #[test] + fn edit_high_square_one_output_one_input() { + // 17¢ output + 1×2¢ input = 19¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1), 1), 19); } #[test] - fn default_batch_costs_17_cents() { - let (credits, cents) = cost_for_batch_with_refs(None, 1); - assert_eq!(credits, Some(17)); - assert_eq!(cents, Some(17)); + fn edit_high_square_one_output_three_inputs() { + // 17¢ output + 3×2¢ input = 23¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1), 3), 23); } #[test] - fn batch_of_one_costs_17_cents() { - let (_c, cents) = cost_for_batch_with_refs(Some(1), 1); - assert_eq!(cents, Some(17)); + fn edit_high_square_one_output_five_inputs() { + // 17¢ output + 5×2¢ input = 27¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1), 5), 27); } #[test] - fn batch_of_two_costs_34_cents() { - let (_c, cents) = cost_for_batch_with_refs(Some(2), 1); - assert_eq!(cents, Some(34)); + fn edit_high_wide_two_outputs_one_input() { + // 2×25¢ output + 1×2¢ input = 52¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(2), 1), 52); } #[test] - fn batch_of_three_costs_51_cents() { - let (_c, cents) = cost_for_batch_with_refs(Some(3), 1); - assert_eq!(cents, Some(51)); + fn edit_medium_wide_two_outputs_five_inputs() { + // 2×7¢ output + 5×2¢ input = 24¢ + assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(2), 5), 24); } #[test] - fn batch_of_four_costs_68_cents() { - let (_c, cents) = cost_for_batch_with_refs(Some(4), 1); - assert_eq!(cents, Some(68)); + fn edit_low_square_one_output_two_inputs() { + // 2¢ output + 2×2¢ input = 6¢ + assert_eq!(cost_edit(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), Some(1), 2), 6); } #[test] - fn batch_above_max_clamps_and_costs_68_cents() { - let (_c, cents) = cost_for_batch_with_refs(Some(9), 1); - assert_eq!(cents, Some(68)); + fn edit_low_tall_four_outputs_one_input() { + // 4×2¢ output + 1×2¢ input = 10¢ + assert_eq!(cost_edit(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), Some(4), 1), 10); + } + + // ── Default quality in edit mode (None → High) ────────────────────── + + #[test] + fn edit_default_quality_square_one_output_one_input() { + // 17¢ output + 2¢ input = 19¢ + assert_eq!(cost_edit(None, Some(CommonAspectRatio::Square), Some(1), 1), 19); } #[test] - fn cost_is_independent_of_image_ref_count() { - for num_refs in [1usize, 2, 3, 5] { - let (_c, cents) = cost_for_batch_with_refs(Some(2), num_refs); + fn edit_default_quality_wide_one_output_one_input() { + // 25¢ output + 2¢ input = 27¢ + assert_eq!(cost_edit(None, Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 27); + } + + // ── Quality × size matrix in edit mode (1 output, 1 input) ────────── + + #[test] + fn edit_low_square() { + // 2¢ + 2¢ = 4¢ + assert_eq!(cost_edit(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), Some(1), 1), 4); + } + + #[test] + fn edit_low_wide() { + // 2¢ + 2¢ = 4¢ + assert_eq!(cost_edit(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 4); + } + + #[test] + fn edit_medium_square() { + // 5¢ + 2¢ = 7¢ + assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(1), 1), 7); + } + + #[test] + fn edit_medium_wide() { + // 7¢ + 2¢ = 9¢ + assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 9); + } + + #[test] + fn edit_medium_tall() { + // 7¢ + 2¢ = 9¢ + assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), Some(1), 1), 9); + } + + #[test] + fn edit_high_square() { + // 17¢ + 2¢ = 19¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1), 1), 19); + } + + #[test] + fn edit_high_wide() { + // 25¢ + 2¢ = 27¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 27); + } + + #[test] + fn edit_high_tall() { + // 25¢ + 2¢ = 27¢ + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), Some(1), 1), 27); + } + + // ── Batch above max clamps ────────────────────────────────────────── + + #[test] + fn edit_batch_above_max_clamps() { + // High square 4×17 + 1×2 = 70¢ (clamped from 9 to 4) + assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(9), 1), 70); + } + + // ── Cost matches text + 2¢ per input image ───────────────────────── + + #[test] + fn edit_and_text_cost_differ_by_input_image_cost() { + for q in [CommonQuality::Low, CommonQuality::Medium, CommonQuality::High] { + let text = distill_text(&make_request(Some("p"), None, Some(q), Some(1), None)) + .cost.cost_in_usd_cents.unwrap(); + let (tokens, hydration) = fake_image_refs(1); + let edit = distill_edit( + &make_request(Some("p"), None, Some(q), Some(1), Some(tokens)), + &hydration, + ).cost.cost_in_usd_cents.unwrap(); assert_eq!( - cents, - Some(34), - "expected 34¢ regardless of {} image refs", - num_refs, + edit - text, + 2, + "expected edit to cost 2¢ more than text for quality {:?}", + q, ); } } - #[test] - fn cost_is_independent_of_aspect_ratio() { - let aspect_ratios = [ - None, - Some(CommonAspectRatio::Square), - Some(CommonAspectRatio::Auto), - Some(CommonAspectRatio::WideSixteenByNine), - Some(CommonAspectRatio::TallNineBySixteen), - ]; - for ar in aspect_ratios { - let (tokens, hydration) = fake_image_refs(2); - let request = make_request(Some("p"), ar, Some(2), Some(tokens)); - let distilled = distill_edit(&request, &hydration); - assert_eq!( - distilled.cost.cost_in_usd_cents, - Some(34), - "expected 34¢ regardless of aspect ratio (got {:?})", - ar, - ); - } - } + // ── Metadata flags ─────────────────────────────────────────────────── #[test] fn cost_metadata_flags_are_default() { let (tokens, hydration) = fake_image_refs(1); - let request = make_request(Some("p"), None, Some(1), Some(tokens)); + let request = make_request(Some("p"), None, None, Some(1), Some(tokens)); let distilled = distill_edit(&request, &hydration); assert!(!distilled.cost.is_free); assert!(!distilled.cost.is_unlimited); assert!(!distilled.cost.is_rate_limited); assert!(!distilled.cost.has_watermark); } - - #[test] - fn edit_and_text_cost_match_for_same_batch() { - for batch in [1u16, 2, 3, 4] { - let text = distill_text(&make_request(Some("p"), None, Some(batch), None)); - let (tokens, hydration) = fake_image_refs(1); - let edit = distill_edit( - &make_request(Some("p"), None, Some(batch), Some(tokens)), - &hydration, - ); - assert_eq!( - text.cost.cost_in_usd_cents, - edit.cost.cost_in_usd_cents, - "text/edit cost diverged at batch={}", - batch, - ); - } - } } } // ──────────────────────────────────────────────────────────────────────────── // PLAN // ──────────────────────────────────────────────────────────────────────────── - // - // The distilled plan is `ImageGenerationPlan::FalGptImage1(PlanFalGptImage1)`. - // It carries: - // - prompt: passthrough - // - image_urls: empty in text mode, populated in edit mode - // - image_size: None / Square / Horizontal / Vertical - // - quality: always Medium (fal plan default) - // - num_images: 1..=4 (clamped under PayMoreUpgrade) - // - // The handler picks t2i vs edit based on whether image_urls is empty. mod plan { use super::*; @@ -347,212 +467,136 @@ mod tests { mod text { use super::*; - fn assert_t2i_for_aspect_ratio)>( - ar: Option, - assertion: F, - ) { - with_text_plan(&make_request(Some("p"), ar, Some(1), None), assertion); + // ── Quality mapping ─────────────────────────────────────────────────── + + #[test] + fn default_quality_is_medium() { + with_text_plan(&make_request(Some("p"), None, None, Some(1), None), |plan| { + assert!(matches!(plan.quality, FalGptImage1Quality::Medium)); + }); } - fn assert_t2i_for_batch)>( - batch: Option, - assertion: F, - ) { - with_text_plan(&make_request(Some("p"), None, batch, None), assertion); + #[test] + fn low_quality_passes_through() { + with_text_plan(&make_request(Some("p"), None, Some(CommonQuality::Low), Some(1), None), |plan| { + assert!(matches!(plan.quality, FalGptImage1Quality::Low)); + }); + } + + #[test] + fn medium_quality_passes_through() { + with_text_plan(&make_request(Some("p"), None, Some(CommonQuality::Medium), Some(1), None), |plan| { + assert!(matches!(plan.quality, FalGptImage1Quality::Medium)); + }); + } + + #[test] + fn high_quality_passes_through() { + with_text_plan(&make_request(Some("p"), None, Some(CommonQuality::High), Some(1), None), |plan| { + assert!(matches!(plan.quality, FalGptImage1Quality::High)); + }); } // ── Mode detection ──────────────────────────────────────────────────── #[test] fn text_mode_has_empty_image_urls() { - with_text_plan(&make_request(Some("p"), None, Some(1), None), |plan| { - assert!(plan.image_urls.is_empty(), "text mode must have no image_urls"); - }); - } - - // ── Quality default ─────────────────────────────────────────────────── - - #[test] - fn quality_defaults_to_medium() { - with_text_plan(&make_request(Some("p"), None, Some(1), None), |plan| { - assert!(matches!(plan.quality, FalGptImage1Quality::Medium)); + with_text_plan(&make_request(Some("p"), None, None, Some(1), None), |plan| { + assert!(plan.image_urls.is_empty()); }); } // ── Image size mappings ─────────────────────────────────────────────── - // - // GPT Image 1 has three sizes. Aspect ratios collapse into them: - // None / Auto* → None (API default) - // Square / SquareHd → Square - // All wide → Horizontal - // All tall → Vertical #[test] fn default_image_size_is_none() { - assert_t2i_for_aspect_ratio(None, |plan| { + with_text_plan(&make_request(Some("p"), None, None, Some(1), None), |plan| { assert!(plan.image_size.is_none()); }); } #[test] - fn auto_yields_none() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::Auto), |plan| { - assert!(plan.image_size.is_none()); - }); - } - - #[test] - fn auto_2k_yields_none() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::Auto2k), |plan| { - assert!(plan.image_size.is_none()); - }); - } - - #[test] - fn auto_4k_yields_none() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::Auto4k), |plan| { - assert!(plan.image_size.is_none()); - }); + fn auto_variants_yield_none() { + for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] { + with_text_plan(&make_request(Some("p"), Some(ar), None, Some(1), None), |plan| { + assert!(plan.image_size.is_none(), "expected None for {:?}", ar); + }); + } } #[test] fn square_yields_square() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::Square), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square))); - }); + for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] { + with_text_plan(&make_request(Some("p"), Some(ar), None, Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square)), "expected Square for {:?}", ar); + }); + } } #[test] - fn square_hd_yields_square() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::SquareHd), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square))); - }); + fn wide_variants_yield_horizontal() { + let wide_ars = [ + CommonAspectRatio::WideFiveByFour, + CommonAspectRatio::WideFourByThree, + CommonAspectRatio::WideThreeByTwo, + CommonAspectRatio::WideSixteenByNine, + CommonAspectRatio::WideTwentyOneByNine, + CommonAspectRatio::Wide, + ]; + for ar in wide_ars { + with_text_plan(&make_request(Some("p"), Some(ar), None, Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal)), "expected Horizontal for {:?}", ar); + }); + } } #[test] - fn wide_5x4_yields_horizontal() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::WideFiveByFour), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_4x3_yields_horizontal() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::WideFourByThree), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_3x2_yields_horizontal() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::WideThreeByTwo), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_16x9_yields_horizontal() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::WideSixteenByNine), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_alias_yields_horizontal() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::Wide), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_21x9_yields_horizontal() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::WideTwentyOneByNine), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn tall_4x5_yields_vertical() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::TallFourByFive), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_3x4_yields_vertical() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::TallThreeByFour), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_2x3_yields_vertical() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::TallTwoByThree), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_9x16_yields_vertical() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::TallNineBySixteen), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_alias_yields_vertical() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::Tall), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_9x21_yields_vertical() { - assert_t2i_for_aspect_ratio(Some(CommonAspectRatio::TallNineByTwentyOne), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); + fn tall_variants_yield_vertical() { + let tall_ars = [ + CommonAspectRatio::TallFourByFive, + CommonAspectRatio::TallThreeByFour, + CommonAspectRatio::TallTwoByThree, + CommonAspectRatio::TallNineBySixteen, + CommonAspectRatio::TallNineByTwentyOne, + CommonAspectRatio::Tall, + ]; + for ar in tall_ars { + with_text_plan(&make_request(Some("p"), Some(ar), None, Some(1), None), |plan| { + assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical)), "expected Vertical for {:?}", ar); + }); + } } // ── Num images mapping ──────────────────────────────────────────────── #[test] fn default_batch_count_is_one() { - assert_t2i_for_batch(None, |plan| { + with_text_plan(&make_request(Some("p"), None, None, None, None), |plan| { assert!(matches!(plan.num_images, FalGptImage1NumImages::One)); }); } #[test] - fn batch_of_one_yields_one() { - assert_t2i_for_batch(Some(1), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::One)); - }); - } - - #[test] - fn batch_of_two_yields_two() { - assert_t2i_for_batch(Some(2), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Two)); - }); - } - - #[test] - fn batch_of_three_yields_three() { - assert_t2i_for_batch(Some(3), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Three)); - }); - } - - #[test] - fn batch_of_four_yields_four() { - assert_t2i_for_batch(Some(4), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Four)); - }); + fn batch_direct_mapping() { + let cases = [ + (1u16, FalGptImage1NumImages::One), + (2, FalGptImage1NumImages::Two), + (3, FalGptImage1NumImages::Three), + (4, FalGptImage1NumImages::Four), + ]; + for (count, expected) in cases { + with_text_plan(&make_request(Some("p"), None, None, Some(count), None), |plan| { + assert!( + std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected), + "expected {:?} for count {}", expected, count, + ); + }); + } } #[test] fn batch_above_four_clamps_to_four() { - assert_t2i_for_batch(Some(9), |plan| { + with_text_plan(&make_request(Some("p"), None, None, Some(9), None), |plan| { assert!(matches!(plan.num_images, FalGptImage1NumImages::Four)); }); } @@ -561,14 +605,14 @@ mod tests { #[test] fn prompt_is_passed_through() { - with_text_plan(&make_request(Some("a corgi in a hat"), None, Some(1), None), |plan| { + with_text_plan(&make_request(Some("a corgi in a hat"), None, None, Some(1), None), |plan| { assert_eq!(plan.prompt, Some("a corgi in a hat")); }); } #[test] fn missing_prompt_is_none() { - with_text_plan(&make_request(None, None, Some(1), None), |plan| { + with_text_plan(&make_request(None, None, None, Some(1), None), |plan| { assert_eq!(plan.prompt, None); }); } @@ -577,37 +621,13 @@ mod tests { mod edit { use super::*; - fn assert_edit_for_aspect_ratio)>( - ar: Option, - assertion: F, - ) { - let (tokens, hydration) = fake_image_refs(1); - with_edit_plan( - &make_request(Some("p"), ar, Some(1), Some(tokens)), - &hydration, - assertion, - ); - } - - fn assert_edit_for_batch)>( - batch: Option, - assertion: F, - ) { - let (tokens, hydration) = fake_image_refs(1); - with_edit_plan( - &make_request(Some("p"), None, batch, Some(tokens)), - &hydration, - assertion, - ); - } - // ── Mode detection / image-url passthrough ──────────────────────────── #[test] fn edit_mode_populates_image_urls() { let (tokens, hydration) = fake_image_refs(1); with_edit_plan( - &make_request(Some("p"), None, Some(1), Some(tokens)), + &make_request(Some("p"), None, None, Some(1), Some(tokens)), &hydration, |plan| { assert_eq!(plan.image_urls.len(), 1); @@ -620,7 +640,7 @@ mod tests { fn edit_mode_with_two_image_refs() { let (tokens, hydration) = fake_image_refs(2); with_edit_plan( - &make_request(Some("p"), None, Some(1), Some(tokens)), + &make_request(Some("p"), None, None, Some(1), Some(tokens)), &hydration, |plan| { assert_eq!(plan.image_urls.len(), 2); @@ -632,7 +652,7 @@ mod tests { fn edit_mode_with_five_image_refs() { let (tokens, hydration) = fake_image_refs(5); with_edit_plan( - &make_request(Some("p"), None, Some(1), Some(tokens)), + &make_request(Some("p"), None, None, Some(1), Some(tokens)), &hydration, |plan| { assert_eq!(plan.image_urls.len(), 5); @@ -643,12 +663,11 @@ mod tests { #[test] fn edit_image_urls_are_hydrated_from_map() { let (tokens, hydration) = fake_image_refs(3); - let expected: Vec = tokens - .iter() + let expected: Vec = tokens.iter() .map(|t| hydration.get(t).unwrap().to_string()) .collect(); with_edit_plan( - &make_request(Some("p"), None, Some(1), Some(tokens.clone())), + &make_request(Some("p"), None, None, Some(1), Some(tokens.clone())), &hydration, |plan| { assert_eq!(plan.image_urls, expected); @@ -656,13 +675,13 @@ mod tests { ); } - // ── Quality default ─────────────────────────────────────────────────── + // ── Quality mapping in edit mode ────────────────────────────────────── #[test] - fn quality_defaults_to_medium() { + fn default_quality_is_medium_in_edit_mode() { let (tokens, hydration) = fake_image_refs(1); with_edit_plan( - &make_request(Some("p"), None, Some(1), Some(tokens)), + &make_request(Some("p"), None, None, Some(1), Some(tokens)), &hydration, |plan| { assert!(matches!(plan.quality, FalGptImage1Quality::Medium)); @@ -670,203 +689,135 @@ mod tests { ); } - // ── Image size mappings ─────────────────────────────────────────────── - #[test] - fn default_image_size_is_none() { - assert_edit_for_aspect_ratio(None, |plan| { - assert!(plan.image_size.is_none()); - }); - } - - #[test] - fn auto_yields_none() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::Auto), |plan| { - assert!(plan.image_size.is_none()); - }); - } - - #[test] - fn auto_2k_yields_none() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::Auto2k), |plan| { - assert!(plan.image_size.is_none()); - }); - } - - #[test] - fn auto_4k_yields_none() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::Auto4k), |plan| { - assert!(plan.image_size.is_none()); - }); - } - - #[test] - fn square_yields_square() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::Square), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square))); - }); - } - - #[test] - fn square_hd_yields_square() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::SquareHd), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square))); - }); - } - - #[test] - fn wide_5x4_yields_horizontal() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::WideFiveByFour), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_4x3_yields_horizontal() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::WideFourByThree), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_3x2_yields_horizontal() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::WideThreeByTwo), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_16x9_yields_horizontal() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::WideSixteenByNine), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_alias_yields_horizontal() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::Wide), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn wide_21x9_yields_horizontal() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::WideTwentyOneByNine), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); - }); - } - - #[test] - fn tall_4x5_yields_vertical() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::TallFourByFive), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_3x4_yields_vertical() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::TallThreeByFour), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_2x3_yields_vertical() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::TallTwoByThree), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_9x16_yields_vertical() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::TallNineBySixteen), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_alias_yields_vertical() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::Tall), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - #[test] - fn tall_9x21_yields_vertical() { - assert_edit_for_aspect_ratio(Some(CommonAspectRatio::TallNineByTwentyOne), |plan| { - assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); - }); - } - - // ── Num images mapping ──────────────────────────────────────────────── - - #[test] - fn default_batch_count_is_one() { - assert_edit_for_batch(None, |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::One)); - }); - } - - #[test] - fn batch_of_one_yields_one() { - assert_edit_for_batch(Some(1), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::One)); - }); - } - - #[test] - fn batch_of_two_yields_two() { - assert_edit_for_batch(Some(2), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Two)); - }); - } - - #[test] - fn batch_of_three_yields_three() { - assert_edit_for_batch(Some(3), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Three)); - }); - } - - #[test] - fn batch_of_four_yields_four() { - assert_edit_for_batch(Some(4), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Four)); - }); - } - - #[test] - fn batch_above_four_clamps_to_four() { - assert_edit_for_batch(Some(9), |plan| { - assert!(matches!(plan.num_images, FalGptImage1NumImages::Four)); - }); - } - - // ── Prompt passthrough ──────────────────────────────────────────────── - - #[test] - fn prompt_is_passed_through() { + fn low_quality_passes_through_in_edit_mode() { let (tokens, hydration) = fake_image_refs(1); with_edit_plan( - &make_request(Some("make it shiny"), None, Some(1), Some(tokens)), + &make_request(Some("p"), None, Some(CommonQuality::Low), Some(1), Some(tokens)), &hydration, |plan| { - assert_eq!(plan.prompt, Some("make it shiny")); + assert!(matches!(plan.quality, FalGptImage1Quality::Low)); }, ); } #[test] - fn missing_prompt_is_none() { + fn high_quality_passes_through_in_edit_mode() { let (tokens, hydration) = fake_image_refs(1); with_edit_plan( - &make_request(None, None, Some(1), Some(tokens)), + &make_request(Some("p"), None, Some(CommonQuality::High), Some(1), Some(tokens)), &hydration, |plan| { - assert_eq!(plan.prompt, None); + assert!(matches!(plan.quality, FalGptImage1Quality::High)); }, ); } + + // ── Image size in edit mode ────────────────────────────────────────── + + #[test] + fn edit_default_image_size_is_none() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, None, Some(1), Some(tokens)), + &hydration, + |plan| { assert!(plan.image_size.is_none()); }, + ); + } + + #[test] + fn edit_square_yields_square() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::Square), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square))); }, + ); + } + + #[test] + fn edit_wide_yields_horizontal() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::WideSixteenByNine), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal))); }, + ); + } + + #[test] + fn edit_tall_yields_vertical() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::TallNineBySixteen), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical))); }, + ); + } + + #[test] + fn edit_auto_yields_none() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), Some(CommonAspectRatio::Auto), None, Some(1), Some(tokens)), + &hydration, + |plan| { assert!(plan.image_size.is_none()); }, + ); + } + + // ── Num images in edit mode ────────────────────────────────────────── + + #[test] + fn edit_default_batch_count_is_one() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, None, None, Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.num_images, FalGptImage1NumImages::One)); }, + ); + } + + #[test] + fn edit_batch_of_four_yields_four() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, None, Some(4), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.num_images, FalGptImage1NumImages::Four)); }, + ); + } + + #[test] + fn edit_batch_above_four_clamps_to_four() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("p"), None, None, Some(9), Some(tokens)), + &hydration, + |plan| { assert!(matches!(plan.num_images, FalGptImage1NumImages::Four)); }, + ); + } + + // ── Prompt passthrough in edit mode ──────────────────────────────────── + + #[test] + fn edit_prompt_is_passed_through() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(Some("make it shiny"), None, None, Some(1), Some(tokens)), + &hydration, + |plan| { assert_eq!(plan.prompt, Some("make it shiny")); }, + ); + } + + #[test] + fn edit_missing_prompt_is_none() { + let (tokens, hydration) = fake_image_refs(1); + with_edit_plan( + &make_request(None, None, None, Some(1), Some(tokens)), + &hydration, + |plan| { assert_eq!(plan.prompt, None); }, + ); + } } } }