gpt-image-1.5 + quality

This commit is contained in:
Brandon Thomas committed 2026-04-09 18:23:23 -04:00
1 parent 2313ef48c2
commit 4dd1a4f2be
7 files changed
+780 -17

No files matched your search

@@ -108,7 +108,7 @@ fn plan_quality(quality: Option<CommonQuality>) -> ArtcraftGptImage1Quality {
Some(CommonQuality::Low) => ArtcraftGptImage1Quality::Low,
Some(CommonQuality::Medium) => ArtcraftGptImage1Quality::Medium,
Some(CommonQuality::High) => ArtcraftGptImage1Quality::High,
None => ArtcraftGptImage1Quality::High, // Legacy handler default
None => ArtcraftGptImage1Quality::High,
}
}
@@ -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;
@@ -30,13 +31,14 @@ pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let quality = plan_quality(request.quality);
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::ArtcraftGptImage1p5(PlanArtcraftGptImage1p5 {
prompt: request.prompt,
image_inputs,
image_size,
quality: GptImage1p5MultiFunctionImageGenQuality::Medium,
quality,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
}))
@@ -48,9 +50,19 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// Omni-gen distillation hydrates media tokens to URLs before running the
// Artcraft cost path. Cost only depends on quality + size + num_images, so
// URL-form inputs are accepted and dropped.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
fn plan_quality(quality: Option<CommonQuality>) -> GptImage1p5MultiFunctionImageGenQuality {
match quality {
Some(CommonQuality::Low) => GptImage1p5MultiFunctionImageGenQuality::Low,
Some(CommonQuality::Medium) => GptImage1p5MultiFunctionImageGenQuality::Medium,
Some(CommonQuality::High) => GptImage1p5MultiFunctionImageGenQuality::High,
None => GptImage1p5MultiFunctionImageGenQuality::High,
}
}
@@ -195,10 +207,10 @@ mod tests {
// ── Quality ──────────────────────────────────────────────────────────────
#[test]
fn quality_defaults_to_medium() {
fn quality_defaults_to_high() {
let request = base_gpt_image_1p5_image_request();
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::Medium));
assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::High));
}
// ── Num images ──────────────────────────────────────────────────────────────
@@ -277,17 +289,16 @@ mod tests {
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_gpt_image_1p5_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1p5")
};
assert!(plan.image_inputs.is_none());
}
#[test]
@@ -86,7 +86,7 @@ fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage1Quality {
Some(CommonQuality::Low) => FalGptImage1Quality::Low,
Some(CommonQuality::Medium) => FalGptImage1Quality::Medium,
Some(CommonQuality::High) => FalGptImage1Quality::High,
None => FalGptImage1Quality::Medium, // Fal API default
None => FalGptImage1Quality::High,
}
}
@@ -286,10 +286,10 @@ mod tests {
// ── Quality default ──────────────────────────────────────────────────────
#[test]
fn quality_defaults_to_medium() {
fn quality_defaults_to_high() {
let request = base_fal_request();
let plan = build_plan(&request);
assert!(matches!(plan.quality, FalGptImage1Quality::Medium));
assert!(matches!(plan.quality, FalGptImage1Quality::High));
}
// ── Image size mapping ───────────────────────────────────────────────────
@@ -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;
@@ -53,13 +54,14 @@ pub fn plan_generate_image_fal_gpt_image_1p5<'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::FalGptImage1p5(PlanFalGptImage1p5 {
prompt: request.prompt,
image_urls,
image_size,
quality: FalGptImage1p5Quality::Medium,
quality,
num_images,
}))
}
@@ -76,6 +78,15 @@ fn resolve_image_list_ref(
}
}
fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage1p5Quality {
match quality {
Some(CommonQuality::Low) => FalGptImage1p5Quality::Low,
Some(CommonQuality::Medium) => FalGptImage1p5Quality::Medium,
Some(CommonQuality::High) => FalGptImage1p5Quality::High,
None => FalGptImage1p5Quality::High,
}
}
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage1p5ImageSize> {
use FalGptImage1p5ImageSize as S;
match aspect_ratio {
@@ -223,6 +223,12 @@ fn build_omni_gen_image_models() -> Vec<OmniGenImageModelDetails> {
CommonAspectRatio::TallTwoByThree,
]),
aspect_ratio_default: Some(CommonAspectRatio::Square),
quality_options: Some(vec![
CommonQuality::High,
CommonQuality::Medium,
CommonQuality::Low,
]),
default_quality: Some(CommonQuality::High),
batch_size_min: Some(1),
batch_size_max: Some(4),
batch_size_default: Some(1),
@@ -3,6 +3,7 @@ mod test_flux_1_schnell;
mod test_flux_pro_1p1;
mod test_flux_pro_1p1_ultra;
mod test_gpt_image_1;
mod test_gpt_image_1p5;
mod test_nano_banana;
mod test_nano_banana_2;
mod test_nano_banana_pro;
@@ -0,0 +1,734 @@
//! Tests for omni-gen against the GPT Image 1.5 model.
//!
//! GPT Image 1.5 is multi-function: text-to-image and image-to-image (edit).
//!
//! Artcraft-tier pricing (what the user is billed):
//! Low – 1¢ (any size)
//! Medium – 3¢ (square/unset) / 5¢ (wide or tall) ← default when quality unset
//! High – 13¢ (square/unset) / 20¢ (wide or tall)
//!
//! 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), Wide (1536×1024), Tall (1024×1536).
//! Auto/unset maps to None (API default).
#[cfg(test)]
mod tests {
use std::collections::HashMap;
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;
use crate::http_server::endpoints::omni_gen::generate::image::distill_image_request::{
distill_image_request, DistilledImageRequest,
};
// ── Helpers ────────────────────────────────────────────────────────────────
fn make_request(
prompt: Option<&str>,
aspect_ratio: Option<CommonAspectRatio>,
quality: Option<CommonQuality>,
image_batch_count: Option<u16>,
image_media_tokens: Option<Vec<MediaFileToken>>,
) -> OmniGenImageCostAndGenerateRequest {
OmniGenImageCostAndGenerateRequest {
idempotency_token: Some("11111111-1111-1111-1111-111111111111".to_string()),
model: Some(CommonImageModel::GptImage1p5),
prompt: prompt.map(|s| s.to_string()),
image_media_tokens,
resolution: None,
aspect_ratio,
quality,
image_batch_count,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
}
}
fn fake_image_refs(n: usize) -> (Vec<MediaFileToken>, HashMap<MediaFileToken, Url>) {
let mut tokens = Vec::with_capacity(n);
let mut map = HashMap::with_capacity(n);
for i in 0..n {
let token = MediaFileToken::new_from_str(&format!("mf_test{:028}", i));
let url = Url::parse(&format!("https://fake.example.com/img{}.png", i)).unwrap();
map.insert(token.clone(), url);
tokens.push(token);
}
(tokens, map)
}
fn distill_text(request: &OmniGenImageCostAndGenerateRequest) -> DistilledImageRequest {
let empty: HashMap<MediaFileToken, Url> = HashMap::new();
distill_image_request(request, Some(&empty))
.expect("distill_image_request should succeed for GPT Image 1.5 (text)")
}
fn distill_edit(
request: &OmniGenImageCostAndGenerateRequest,
hydration: &HashMap<MediaFileToken, Url>,
) -> DistilledImageRequest {
distill_image_request(request, Some(hydration))
.expect("distill_image_request should succeed for GPT Image 1.5 (edit)")
}
// ────────────────────────────────────────────────────────────────────────────
// COST
// ────────────────────────────────────────────────────────────────────────────
mod cost {
use super::*;
mod text {
use super::*;
fn cost(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: Option<u16>,
) -> u64 {
let request = make_request(Some("p"), aspect_ratio, quality, batch, None);
distill_text(&request).cost.cost_in_usd_cents.unwrap()
}
// ── Default quality (None → Medium) ──────────────────────────────────
#[test]
fn default_quality_square_costs_3() {
assert_eq!(cost(None, Some(CommonAspectRatio::Square), Some(1)), 3);
}
#[test]
fn default_quality_wide_costs_5() {
assert_eq!(cost(None, Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 5);
}
#[test]
fn default_quality_tall_costs_5() {
assert_eq!(cost(None, Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 5);
}
#[test]
fn default_quality_unset_size_costs_3() {
assert_eq!(cost(None, None, Some(1)), 3);
}
#[test]
fn default_quality_auto_costs_3() {
assert_eq!(cost(None, Some(CommonAspectRatio::Auto), Some(1)), 3);
}
// ── Low quality (1¢/image regardless of size) ────────────────────────
#[test]
fn low_square_costs_1() {
assert_eq!(cost(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), Some(1)), 1);
}
#[test]
fn low_wide_costs_1() {
assert_eq!(cost(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 1);
}
#[test]
fn low_tall_costs_1() {
assert_eq!(cost(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 1);
}
#[test]
fn low_unset_costs_1() {
assert_eq!(cost(Some(CommonQuality::Low), None, Some(1)), 1);
}
#[test]
fn low_four_images_costs_4() {
assert_eq!(cost(Some(CommonQuality::Low), None, Some(4)), 4);
}
#[test]
fn low_batch_above_max_clamps() {
assert_eq!(cost(Some(CommonQuality::Low), None, Some(7)), 4);
}
// ── Medium quality (3¢ square, 5¢ wide/tall) ─────────────────────────
#[test]
fn medium_square_costs_3() {
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(1)), 3);
}
#[test]
fn medium_wide_costs_5() {
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 5);
}
#[test]
fn medium_tall_costs_5() {
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 5);
}
#[test]
fn medium_square_four_images_costs_12() {
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(4)), 12);
}
#[test]
fn medium_wide_four_images_costs_20() {
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(4)), 20);
}
// ── High quality (13¢ square, 20¢ wide/tall) ─────────────────────────
#[test]
fn high_square_costs_13() {
assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1)), 13);
}
#[test]
fn high_wide_costs_20() {
assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(1)), 20);
}
#[test]
fn high_tall_costs_20() {
assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), Some(1)), 20);
}
#[test]
fn high_unset_costs_13() {
assert_eq!(cost(Some(CommonQuality::High), None, Some(1)), 13);
}
#[test]
fn high_square_four_images_costs_52() {
assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(4)), 52);
}
#[test]
fn high_wide_four_images_costs_80() {
assert_eq!(cost(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(4)), 80);
}
// ── Batch scaling ────────────────────────────────────────────────────
#[test]
fn medium_square_batch_scaling() {
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(1)), 3);
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(2)), 6);
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(3)), 9);
assert_eq!(cost(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(4)), 12);
}
// ── Metadata flags ───────────────────────────────────────────────────
#[test]
fn cost_metadata_flags_are_default() {
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);
}
#[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_edit(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: Option<u16>,
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()
}
// ── Edit mode costs match text mode (no per-input-image surcharge for
// 1.5 — the artcraft cost estimator for 1.5 does not add input
// image token costs like 1.0 does) ──────────────────────────────────
#[test]
fn edit_default_quality_square_one_output_one_input() {
assert_eq!(cost_edit(None, Some(CommonAspectRatio::Square), Some(1), 1), 3);
}
#[test]
fn edit_default_quality_wide_one_output_one_input() {
assert_eq!(cost_edit(None, Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 5);
}
#[test]
fn edit_low_square_one_output_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), Some(1), 1), 1);
}
#[test]
fn edit_medium_square_one_output_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), Some(1), 1), 3);
}
#[test]
fn edit_medium_wide_one_output_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 5);
}
#[test]
fn edit_medium_tall_one_output_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), Some(1), 1), 5);
}
#[test]
fn edit_high_square_one_output_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(1), 1), 13);
}
#[test]
fn edit_high_wide_one_output_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), Some(1), 1), 20);
}
#[test]
fn edit_high_square_four_outputs_one_input() {
assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(4), 1), 52);
}
#[test]
fn edit_cost_independent_of_input_image_count() {
for num_refs in [1usize, 2, 3, 5] {
assert_eq!(
cost_edit(Some(CommonQuality::Medium), None, Some(2), num_refs),
6,
"expected 6¢ regardless of {} input images",
num_refs,
);
}
}
#[test]
fn edit_batch_above_max_clamps() {
assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), Some(9), 1), 52);
}
#[test]
fn edit_and_text_cost_match() {
for q in [CommonQuality::Low, CommonQuality::Medium, CommonQuality::High] {
let text = distill_text(&make_request(Some("p"), None, Some(q), Some(2), None))
.cost.cost_in_usd_cents.unwrap();
let (tokens, hydration) = fake_image_refs(3);
let edit = distill_edit(
&make_request(Some("p"), None, Some(q), Some(2), Some(tokens)),
&hydration,
).cost.cost_in_usd_cents.unwrap();
assert_eq!(
text, edit,
"expected text/edit cost to match for quality {:?}", q,
);
}
}
#[test]
fn cost_metadata_flags_are_default() {
let (tokens, hydration) = fake_image_refs(1);
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);
}
}
}
// ────────────────────────────────────────────────────────────────────────────
// PLAN
// ────────────────────────────────────────────────────────────────────────────
mod plan {
use super::*;
use artcraft_router::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_router::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::{
FalGptImage1p5ImageSize, FalGptImage1p5NumImages, FalGptImage1p5Quality, PlanFalGptImage1p5,
};
fn with_text_plan<F: FnOnce(&PlanFalGptImage1p5<'_>)>(
request: &OmniGenImageCostAndGenerateRequest,
assertion: F,
) {
let distilled = distill_text(request);
match distilled.plan() {
ImageGenerationPlan::FalGptImage1p5(plan) => assertion(plan),
other => panic!("expected ImageGenerationPlan::FalGptImage1p5, got {:?}", other),
}
}
fn with_edit_plan<F: FnOnce(&PlanFalGptImage1p5<'_>)>(
request: &OmniGenImageCostAndGenerateRequest,
hydration: &HashMap<MediaFileToken, Url>,
assertion: F,
) {
let distilled = distill_edit(request, hydration);
match distilled.plan() {
ImageGenerationPlan::FalGptImage1p5(plan) => assertion(plan),
other => panic!("expected ImageGenerationPlan::FalGptImage1p5, got {:?}", other),
}
}
mod text {
use super::*;
// ── 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, FalGptImage1p5Quality::Medium));
});
}
#[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, FalGptImage1p5Quality::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, FalGptImage1p5Quality::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, FalGptImage1p5Quality::High));
});
}
// ── Mode detection ────────────────────────────────────────────────────
#[test]
fn text_mode_has_empty_image_urls() {
with_text_plan(&make_request(Some("p"), None, None, Some(1), None), |plan| {
assert!(plan.image_urls.is_empty());
});
}
// ── Image size mappings ───────────────────────────────────────────────
#[test]
fn default_image_size_is_none() {
with_text_plan(&make_request(Some("p"), None, None, Some(1), None), |plan| {
assert!(plan.image_size.is_none());
});
}
#[test]
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() {
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(FalGptImage1p5ImageSize::Square)), "expected Square for {:?}", ar);
});
}
}
#[test]
fn wide_variants_yield_wide() {
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(FalGptImage1p5ImageSize::Wide)), "expected Wide for {:?}", ar);
});
}
}
#[test]
fn tall_variants_yield_tall() {
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(FalGptImage1p5ImageSize::Tall)), "expected Tall for {:?}", ar);
});
}
}
// ── Num images mapping ────────────────────────────────────────────────
#[test]
fn default_batch_count_is_one() {
with_text_plan(&make_request(Some("p"), None, None, None, None), |plan| {
assert!(matches!(plan.num_images, FalGptImage1p5NumImages::One));
});
}
#[test]
fn batch_direct_mapping() {
let cases = [
(1u16, FalGptImage1p5NumImages::One),
(2, FalGptImage1p5NumImages::Two),
(3, FalGptImage1p5NumImages::Three),
(4, FalGptImage1p5NumImages::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() {
with_text_plan(&make_request(Some("p"), None, None, Some(9), None), |plan| {
assert!(matches!(plan.num_images, FalGptImage1p5NumImages::Four));
});
}
// ── Prompt passthrough ────────────────────────────────────────────────
#[test]
fn prompt_is_passed_through() {
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, None, Some(1), None), |plan| {
assert_eq!(plan.prompt, None);
});
}
}
mod edit {
use super::*;
// ── 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, None, Some(1), Some(tokens)),
&hydration,
|plan| {
assert_eq!(plan.image_urls.len(), 1);
assert!(plan.image_urls[0].starts_with("https://fake.example.com/"));
},
);
}
#[test]
fn edit_mode_with_two_image_refs() {
let (tokens, hydration) = fake_image_refs(2);
with_edit_plan(
&make_request(Some("p"), None, None, Some(1), Some(tokens)),
&hydration,
|plan| { assert_eq!(plan.image_urls.len(), 2); },
);
}
#[test]
fn edit_mode_with_five_image_refs() {
let (tokens, hydration) = fake_image_refs(5);
with_edit_plan(
&make_request(Some("p"), None, None, Some(1), Some(tokens)),
&hydration,
|plan| { assert_eq!(plan.image_urls.len(), 5); },
);
}
#[test]
fn edit_image_urls_are_hydrated_from_map() {
let (tokens, hydration) = fake_image_refs(3);
let expected: Vec<String> = tokens.iter()
.map(|t| hydration.get(t).unwrap().to_string())
.collect();
with_edit_plan(
&make_request(Some("p"), None, None, Some(1), Some(tokens.clone())),
&hydration,
|plan| { assert_eq!(plan.image_urls, expected); },
);
}
// ── Quality mapping in edit mode ──────────────────────────────────────
#[test]
fn default_quality_is_medium_in_edit_mode() {
let (tokens, hydration) = fake_image_refs(1);
with_edit_plan(
&make_request(Some("p"), None, None, Some(1), Some(tokens)),
&hydration,
|plan| { assert!(matches!(plan.quality, FalGptImage1p5Quality::Medium)); },
);
}
#[test]
fn low_quality_passes_through_in_edit_mode() {
let (tokens, hydration) = fake_image_refs(1);
with_edit_plan(
&make_request(Some("p"), None, Some(CommonQuality::Low), Some(1), Some(tokens)),
&hydration,
|plan| { assert!(matches!(plan.quality, FalGptImage1p5Quality::Low)); },
);
}
#[test]
fn high_quality_passes_through_in_edit_mode() {
let (tokens, hydration) = fake_image_refs(1);
with_edit_plan(
&make_request(Some("p"), None, Some(CommonQuality::High), Some(1), Some(tokens)),
&hydration,
|plan| { assert!(matches!(plan.quality, FalGptImage1p5Quality::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(FalGptImage1p5ImageSize::Square))); },
);
}
#[test]
fn edit_wide_yields_wide() {
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(FalGptImage1p5ImageSize::Wide))); },
);
}
#[test]
fn edit_tall_yields_tall() {
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(FalGptImage1p5ImageSize::Tall))); },
);
}
#[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, FalGptImage1p5NumImages::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, FalGptImage1p5NumImages::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, FalGptImage1p5NumImages::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); },
);
}
}
}
}