gpt-image-1 + quality

This commit is contained in:
Brandon Thomas committed 2026-04-09 17:01:36 -04:00
1 parent 8c34b87cd9
commit c90b2390f7
51 files changed
+987 -672

No files matched your search

@@ -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,
}
@@ -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;
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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<CommonAspectRatio>) -> 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<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
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<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
num_input_images: usize,
) -> u64 {
let tokens: Vec<MediaFileToken> = (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);
}
}
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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,
@@ -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<FalGptImage1ImageSize>,
num_images: FalGptImage1NumImages,
@@ -51,113 +69,170 @@ mod tests {
}
}
fn cents(
fn edit_plan(
quality: FalGptImage1Quality,
image_size: Option<FalGptImage1ImageSize>,
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<FalGptImage1ImageSize>,
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<FalGptImage1ImageSize>,
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,
@@ -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,
@@ -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()
@@ -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()
@@ -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),
@@ -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()
@@ -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()
@@ -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()
};
@@ -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()
@@ -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()
};
@@ -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)),
@@ -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()
};
@@ -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()
};
@@ -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()
@@ -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,
@@ -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()
};
@@ -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<CommonAspectRatio>,
/// The quality level for generation. Not all models use this.
pub quality: Option<CommonQuality>,
/// How many images to generate.
pub image_batch_count: Option<u16>,
@@ -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()
@@ -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()
@@ -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()
@@ -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()
@@ -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()
@@ -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<MediaFileToken>>,
/// 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<ArtcraftGptImage1Size>,
/// 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<ImageListRef<'_>>) -> 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<CommonQuality>) -> 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).
@@ -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()
@@ -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()
@@ -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()
@@ -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()
@@ -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()
@@ -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<CommonQuality>) -> 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<CommonAspectRatio>) -> Option<FalGptImage1ImageSize> {
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,
@@ -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()
};
@@ -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,
@@ -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),
@@ -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<CommonQualityRouter, AdvancedCommonWebError> {
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<GenerateImageRequest<'_>, 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,
@@ -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,