mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-12 17:01:05 +00:00
artcraft router for images - pipeline v2 (#1580)
This commit is contained in:
1 parent
2b0c5b9085
commit
46b3c92c8b
170 files changed
+7762
-327
No files matched your search
+1
-1
@@ -8,7 +8,7 @@ use enums::common::generation::common_resolution::CommonResolution;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
/// Shared request body for both the image cost estimate and image generation endpoints.
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, ToSchema, Debug)]
|
||||
pub struct OmniGenImageCostAndGenerateRequest {
|
||||
/// REQUIRED (even if marked optional)
|
||||
/// Idempotency token to prevent duplicate requests.
|
||||
|
||||
+73
@@ -10,13 +10,36 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::build::build_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::build::build_artcraft_flux_1_schnell;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::build::build_artcraft_flux_2_lora_angles;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::build::build_artcraft_flux_pro_1p1;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::build::build_artcraft_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::build::build_artcraft_gpt_image_1;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::build::build_artcraft_gpt_image_1p5;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::build::build_artcraft_gpt_image_2;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::build::build_artcraft_nano_banana;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::build::build_artcraft_nano_banana_2;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::build::build_artcraft_nano_banana_pro;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::build::build_artcraft_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::build::build_artcraft_seedream_4;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::build::build_artcraft_seedream_4p5;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::build::build_artcraft_seedream_5_lite;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::build::build_fal_flux_2_lora_angles;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::build::build_fal_flux_pro_1p1;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::build::build_fal_flux_pro_1p1_ultra;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana::build::build_fal_nano_banana;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
|
||||
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::build::build_fal_qwen_edit_2511_angles;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_4::build::build_fal_seedream_4;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_4p5::build::build_fal_seedream_4p5;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::build::build_fal_seedream_5_lite;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::plan_generate_image_artcraft_flux_1_schnell;
|
||||
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::plan_generate_image_artcraft_flux_2_lora_angles;
|
||||
@@ -100,26 +123,76 @@ impl GenerateImageRequestBuilder {
|
||||
|
||||
pub fn use_new_builder(&self) -> bool {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Artcraft, CommonImageModel::Flux1Dev) => true,
|
||||
(Provider::Artcraft, CommonImageModel::Flux1Schnell) => true,
|
||||
(Provider::Artcraft, CommonImageModel::FluxPro11) => true,
|
||||
(Provider::Artcraft, CommonImageModel::FluxPro11Ultra) => true,
|
||||
(Provider::Artcraft, CommonImageModel::GptImage1) => true,
|
||||
(Provider::Artcraft, CommonImageModel::GptImage1p5) => true,
|
||||
(Provider::Artcraft, CommonImageModel::GptImage2) => true,
|
||||
(Provider::Artcraft, CommonImageModel::NanoBanana) => true,
|
||||
(Provider::Artcraft, CommonImageModel::NanoBanana2) => true,
|
||||
(Provider::Artcraft, CommonImageModel::NanoBananaPro) => true,
|
||||
(Provider::Artcraft, CommonImageModel::Seedream4) => true,
|
||||
(Provider::Artcraft, CommonImageModel::Seedream4p5) => true,
|
||||
(Provider::Artcraft, CommonImageModel::Seedream5Lite) => true,
|
||||
(Provider::Artcraft, CommonImageModel::QwenEdit2511Angles) => true,
|
||||
(Provider::Artcraft, CommonImageModel::Flux2LoraAngles) => true,
|
||||
|
||||
(Provider::Fal, CommonImageModel::Flux1Dev) => true,
|
||||
(Provider::Fal, CommonImageModel::Flux1Schnell) => true,
|
||||
(Provider::Fal, CommonImageModel::FluxPro11) => true,
|
||||
(Provider::Fal, CommonImageModel::FluxPro11Ultra) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage1) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage1p5) => true,
|
||||
(Provider::Fal, CommonImageModel::GptImage2) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBanana) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBanana2) => true,
|
||||
(Provider::Fal, CommonImageModel::NanoBananaPro) => true,
|
||||
(Provider::Fal, CommonImageModel::Seedream4) => true,
|
||||
(Provider::Fal, CommonImageModel::Seedream4p5) => true,
|
||||
(Provider::Fal, CommonImageModel::Seedream5Lite) => true,
|
||||
(Provider::Fal, CommonImageModel::QwenEdit2511Angles) => true,
|
||||
(Provider::Fal, CommonImageModel::Flux2LoraAngles) => true,
|
||||
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build2(self) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
(Provider::Artcraft, CommonImageModel::Flux1Dev) => build_artcraft_flux_1_dev(self),
|
||||
(Provider::Artcraft, CommonImageModel::Flux1Schnell) => build_artcraft_flux_1_schnell(self),
|
||||
(Provider::Artcraft, CommonImageModel::FluxPro11) => build_artcraft_flux_pro_1p1(self),
|
||||
(Provider::Artcraft, CommonImageModel::FluxPro11Ultra) => build_artcraft_flux_pro_1p1_ultra(self),
|
||||
(Provider::Artcraft, CommonImageModel::GptImage1) => build_artcraft_gpt_image_1(self),
|
||||
(Provider::Artcraft, CommonImageModel::GptImage1p5) => build_artcraft_gpt_image_1p5(self),
|
||||
(Provider::Artcraft, CommonImageModel::GptImage2) => build_artcraft_gpt_image_2(self),
|
||||
(Provider::Artcraft, CommonImageModel::NanoBanana) => build_artcraft_nano_banana(self),
|
||||
(Provider::Artcraft, CommonImageModel::NanoBanana2) => build_artcraft_nano_banana_2(self),
|
||||
(Provider::Artcraft, CommonImageModel::NanoBananaPro) => build_artcraft_nano_banana_pro(self),
|
||||
(Provider::Artcraft, CommonImageModel::Seedream4) => build_artcraft_seedream_4(self),
|
||||
(Provider::Artcraft, CommonImageModel::Seedream4p5) => build_artcraft_seedream_4p5(self),
|
||||
(Provider::Artcraft, CommonImageModel::Seedream5Lite) => build_artcraft_seedream_5_lite(self),
|
||||
(Provider::Artcraft, CommonImageModel::QwenEdit2511Angles) => build_artcraft_qwen_edit_2511_angles(self),
|
||||
(Provider::Artcraft, CommonImageModel::Flux2LoraAngles) => build_artcraft_flux_2_lora_angles(self),
|
||||
|
||||
(Provider::Fal, CommonImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
|
||||
(Provider::Fal, CommonImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
|
||||
(Provider::Fal, CommonImageModel::FluxPro11) => build_fal_flux_pro_1p1(self),
|
||||
(Provider::Fal, CommonImageModel::FluxPro11Ultra) => build_fal_flux_pro_1p1_ultra(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage1) => build_fal_gpt_image_1(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage1p5) => build_fal_gpt_image_1p5(self),
|
||||
(Provider::Fal, CommonImageModel::GptImage2) => build_fal_gpt_image_2(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBanana) => build_fal_nano_banana(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBanana2) => build_fal_nano_banana_2(self),
|
||||
(Provider::Fal, CommonImageModel::NanoBananaPro) => build_fal_nano_banana_pro(self),
|
||||
(Provider::Fal, CommonImageModel::Seedream4) => build_fal_seedream_4(self),
|
||||
(Provider::Fal, CommonImageModel::Seedream4p5) => build_fal_seedream_4p5(self),
|
||||
(Provider::Fal, CommonImageModel::Seedream5Lite) => build_fal_seedream_5_lite(self),
|
||||
(Provider::Fal, CommonImageModel::QwenEdit2511Angles) => build_fal_qwen_edit_2511_angles(self),
|
||||
(Provider::Fal, CommonImageModel::Flux2LoraAngles) => build_fal_flux_2_lora_angles(self),
|
||||
|
||||
_ => self.unsupported_provider_and_model(),
|
||||
}
|
||||
}
|
||||
|
||||
+252
-14
@@ -3,73 +3,274 @@ use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::cost::ArtcraftFlux1DevCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::cost::ArtcraftFlux1SchnellCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::cost::ArtcraftFlux2LoraAnglesCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::cost::ArtcraftFluxPro1p1CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::cost::ArtcraftFluxPro1p1UltraCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::cost::ArtcraftGptImage1CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::cost::ArtcraftGptImage1p5CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::cost::ArtcraftGptImage2CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::cost::ArtcraftNanoBananaCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::cost::ArtcraftNanoBanana2CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::request::ArtcraftNanoBanana2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::cost::ArtcraftNanoBananaProCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::request::ArtcraftNanoBananaProRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::cost::ArtcraftQwenEdit2511AnglesCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::request::ArtcraftQwenEdit2511AnglesRequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::cost::ArtcraftSeedream4CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::request::ArtcraftSeedream4RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::cost::ArtcraftSeedream4p5CostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::request::ArtcraftSeedream4p5RequestState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::cost::ArtcraftSeedream5LiteCostState;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::request::ArtcraftSeedream5LiteRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::cost::FalFlux1DevCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::cost::FalFlux2LoraAnglesCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::request::FalFlux2LoraAnglesRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::cost::FalFluxPro1p1CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::cost::FalFluxPro1p1UltraCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::cost::FalGptImage2CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana::cost::FalNanoBananaCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana::request::FalNanoBananaRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::cost::FalQwenEdit2511AnglesCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::request::FalQwenEdit2511AnglesRequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_4::cost::FalSeedream4CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_4::request::FalSeedream4RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_4p5::cost::FalSeedream4p5CostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_4p5::request::FalSeedream4p5RequestState;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::cost::FalSeedream5LiteCostState;
|
||||
use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::request::FalSeedream5LiteRequestState;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum ImageGenerationRequest {
|
||||
// ── Artcraft provider (omni-gen image endpoint) ──
|
||||
ArtcraftFlux1Dev(ArtcraftFlux1DevRequestState),
|
||||
ArtcraftFlux1Schnell(ArtcraftFlux1SchnellRequestState),
|
||||
ArtcraftFluxPro1p1(ArtcraftFluxPro1p1RequestState),
|
||||
ArtcraftFluxPro1p1Ultra(ArtcraftFluxPro1p1UltraRequestState),
|
||||
ArtcraftGptImage1(ArtcraftGptImage1RequestState),
|
||||
ArtcraftGptImage1p5(ArtcraftGptImage1p5RequestState),
|
||||
ArtcraftGptImage2(ArtcraftGptImage2RequestState),
|
||||
ArtcraftNanoBanana(ArtcraftNanoBananaRequestState),
|
||||
ArtcraftNanoBanana2(ArtcraftNanoBanana2RequestState),
|
||||
ArtcraftNanoBananaPro(ArtcraftNanoBananaProRequestState),
|
||||
ArtcraftSeedream4(ArtcraftSeedream4RequestState),
|
||||
ArtcraftSeedream4p5(ArtcraftSeedream4p5RequestState),
|
||||
ArtcraftSeedream5Lite(ArtcraftSeedream5LiteRequestState),
|
||||
ArtcraftQwenEdit2511Angles(ArtcraftQwenEdit2511AnglesRequestState),
|
||||
ArtcraftFlux2LoraAngles(ArtcraftFlux2LoraAnglesRequestState),
|
||||
|
||||
// ── Fal provider ──
|
||||
FalFlux1Dev(FalFlux1DevRequestState),
|
||||
FalFlux1Schnell(FalFlux1SchnellRequestState),
|
||||
FalFluxPro1p1(FalFluxPro1p1RequestState),
|
||||
FalFluxPro1p1Ultra(FalFluxPro1p1UltraRequestState),
|
||||
FalGptImage1(FalGptImage1RequestState),
|
||||
FalGptImage1p5(FalGptImage1p5RequestState),
|
||||
FalGptImage2(FalGptImage2RequestState),
|
||||
FalNanoBanana(FalNanoBananaRequestState),
|
||||
FalNanoBanana2(FalNanoBanana2RequestState),
|
||||
FalNanoBananaPro(FalNanoBananaProRequestState),
|
||||
FalSeedream4(FalSeedream4RequestState),
|
||||
FalSeedream4p5(FalSeedream4p5RequestState),
|
||||
FalSeedream5Lite(FalSeedream5LiteRequestState),
|
||||
FalQwenEdit2511Angles(FalQwenEdit2511AnglesRequestState),
|
||||
FalFlux2LoraAngles(FalFlux2LoraAnglesRequestState),
|
||||
}
|
||||
|
||||
impl ImageGenerationRequest {
|
||||
pub fn get_provider(&self) -> Provider {
|
||||
match self {
|
||||
Self::ArtcraftFlux1Dev(_) => Provider::Artcraft,
|
||||
Self::ArtcraftFlux1Schnell(_) => Provider::Artcraft,
|
||||
Self::ArtcraftFluxPro1p1(_) => Provider::Artcraft,
|
||||
Self::ArtcraftFluxPro1p1Ultra(_) => Provider::Artcraft,
|
||||
Self::ArtcraftGptImage1(_) => Provider::Artcraft,
|
||||
Self::ArtcraftGptImage1p5(_) => Provider::Artcraft,
|
||||
Self::ArtcraftGptImage2(_) => Provider::Artcraft,
|
||||
Self::ArtcraftNanoBanana(_) => Provider::Artcraft,
|
||||
Self::ArtcraftNanoBanana2(_) => Provider::Artcraft,
|
||||
Self::ArtcraftNanoBananaPro(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedream4(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedream4p5(_) => Provider::Artcraft,
|
||||
Self::ArtcraftSeedream5Lite(_) => Provider::Artcraft,
|
||||
Self::ArtcraftQwenEdit2511Angles(_) => Provider::Artcraft,
|
||||
Self::ArtcraftFlux2LoraAngles(_) => Provider::Artcraft,
|
||||
|
||||
Self::FalFlux1Dev(_) => Provider::Fal,
|
||||
Self::FalFlux1Schnell(_) => Provider::Fal,
|
||||
Self::FalFluxPro1p1(_) => Provider::Fal,
|
||||
Self::FalFluxPro1p1Ultra(_) => Provider::Fal,
|
||||
Self::FalGptImage1(_) => Provider::Fal,
|
||||
Self::FalGptImage1p5(_) => Provider::Fal,
|
||||
Self::FalGptImage2(_) => Provider::Fal,
|
||||
Self::FalNanoBanana(_) => Provider::Fal,
|
||||
Self::FalNanoBanana2(_) => Provider::Fal,
|
||||
Self::FalNanoBananaPro(_) => Provider::Fal,
|
||||
Self::FalSeedream4(_) => Provider::Fal,
|
||||
Self::FalSeedream4p5(_) => Provider::Fal,
|
||||
Self::FalSeedream5Lite(_) => Provider::Fal,
|
||||
Self::FalQwenEdit2511Angles(_) => Provider::Fal,
|
||||
Self::FalFlux2LoraAngles(_) => Provider::Fal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
Self::FalFlux1Dev(request) => {
|
||||
Ok(FalFlux1DevCostState::from_request(request).estimate_cost())
|
||||
// ── Artcraft ──
|
||||
Self::ArtcraftFlux1Dev(request) => {
|
||||
Ok(ArtcraftFlux1DevCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalFlux1Schnell(request) => {
|
||||
Ok(FalFlux1SchnellCostState::from_request(request).estimate_cost())
|
||||
Self::ArtcraftFlux1Schnell(request) => {
|
||||
Ok(ArtcraftFlux1SchnellCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage1(request) => {
|
||||
Ok(FalGptImage1CostState::from_request(request).estimate_cost())
|
||||
Self::ArtcraftFluxPro1p1(request) => {
|
||||
Ok(ArtcraftFluxPro1p1CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage1p5(request) => {
|
||||
Ok(FalGptImage1p5CostState::from_request(request).estimate_cost())
|
||||
Self::ArtcraftFluxPro1p1Ultra(request) => {
|
||||
Ok(ArtcraftFluxPro1p1UltraCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalGptImage2(request) => {
|
||||
Ok(FalGptImage2CostState::from_request(request).estimate_cost())
|
||||
Self::ArtcraftGptImage1(request) => {
|
||||
Ok(ArtcraftGptImage1CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalNanoBanana2(request) => {
|
||||
Ok(FalNanoBanana2CostState::from_request(request).estimate_cost())
|
||||
Self::ArtcraftGptImage1p5(request) => {
|
||||
Ok(ArtcraftGptImage1p5CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::FalNanoBananaPro(request) => {
|
||||
Ok(FalNanoBananaProCostState::from_request(request).estimate_cost())
|
||||
Self::ArtcraftGptImage2(request) => {
|
||||
Ok(ArtcraftGptImage2CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftNanoBanana(request) => {
|
||||
Ok(ArtcraftNanoBananaCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftNanoBanana2(request) => {
|
||||
Ok(ArtcraftNanoBanana2CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftNanoBananaPro(request) => {
|
||||
Ok(ArtcraftNanoBananaProCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftSeedream4(request) => {
|
||||
Ok(ArtcraftSeedream4CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftSeedream4p5(request) => {
|
||||
Ok(ArtcraftSeedream4p5CostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftSeedream5Lite(request) => {
|
||||
Ok(ArtcraftSeedream5LiteCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftQwenEdit2511Angles(request) => {
|
||||
Ok(ArtcraftQwenEdit2511AnglesCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
Self::ArtcraftFlux2LoraAngles(request) => {
|
||||
Ok(ArtcraftFlux2LoraAnglesCostState::from_request(request).estimate_cost())
|
||||
}
|
||||
|
||||
// ── Fal ──
|
||||
Self::FalFlux1Dev(request) => Ok(FalFlux1DevCostState::from_request(request).estimate_cost()),
|
||||
Self::FalFlux1Schnell(request) => Ok(FalFlux1SchnellCostState::from_request(request).estimate_cost()),
|
||||
Self::FalFluxPro1p1(request) => Ok(FalFluxPro1p1CostState::from_request(request).estimate_cost()),
|
||||
Self::FalFluxPro1p1Ultra(request) => Ok(FalFluxPro1p1UltraCostState::from_request(request).estimate_cost()),
|
||||
Self::FalGptImage1(request) => Ok(FalGptImage1CostState::from_request(request).estimate_cost()),
|
||||
Self::FalGptImage1p5(request) => Ok(FalGptImage1p5CostState::from_request(request).estimate_cost()),
|
||||
Self::FalGptImage2(request) => Ok(FalGptImage2CostState::from_request(request).estimate_cost()),
|
||||
Self::FalNanoBanana(request) => Ok(FalNanoBananaCostState::from_request(request).estimate_cost()),
|
||||
Self::FalNanoBanana2(request) => Ok(FalNanoBanana2CostState::from_request(request).estimate_cost()),
|
||||
Self::FalNanoBananaPro(request) => Ok(FalNanoBananaProCostState::from_request(request).estimate_cost()),
|
||||
Self::FalSeedream4(request) => Ok(FalSeedream4CostState::from_request(request).estimate_cost()),
|
||||
Self::FalSeedream4p5(request) => Ok(FalSeedream4p5CostState::from_request(request).estimate_cost()),
|
||||
Self::FalSeedream5Lite(request) => Ok(FalSeedream5LiteCostState::from_request(request).estimate_cost()),
|
||||
Self::FalQwenEdit2511Angles(request) => Ok(FalQwenEdit2511AnglesCostState::from_request(request).estimate_cost()),
|
||||
Self::FalFlux2LoraAngles(request) => Ok(FalFlux2LoraAnglesCostState::from_request(request).estimate_cost()),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_request(&self, client: &RouterClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
// ── Artcraft (omni-gen image endpoint) ──
|
||||
Self::ArtcraftFlux1Dev(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftFlux1Schnell(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftFluxPro1p1(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftFluxPro1p1Ultra(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftGptImage1(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftGptImage1p5(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftGptImage2(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftNanoBanana(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftNanoBanana2(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftNanoBananaPro(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftSeedream4(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftSeedream4p5(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftSeedream5Lite(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftQwenEdit2511Angles(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
Self::ArtcraftFlux2LoraAngles(request) => {
|
||||
let artcraft_client = client.get_artcraft_client_ref()?;
|
||||
request.send(artcraft_client).await
|
||||
}
|
||||
|
||||
// ── Fal ──
|
||||
Self::FalFlux1Dev(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
@@ -78,6 +279,15 @@ impl ImageGenerationRequest {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalFluxPro1p1(request) => {
|
||||
// Flux Pro 1.1 is webhook-required — uses the webhook-only RouterFalClient.
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalFluxPro1p1Ultra(request) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalGptImage1(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
@@ -90,6 +300,11 @@ impl ImageGenerationRequest {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalNanoBanana(request) => {
|
||||
// nano_banana (Gemini 2.5 Flash) is webhook-required.
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalNanoBanana2(request) => {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
@@ -98,6 +313,29 @@ impl ImageGenerationRequest {
|
||||
let fal_client = client.get_fal_webhook_optional_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalSeedream4(request) => {
|
||||
// Seedream v4 is webhook-required.
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalSeedream4p5(request) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalSeedream5Lite(request) => {
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalQwenEdit2511Angles(request) => {
|
||||
// Webhook-required endpoint.
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
Self::FalFlux2LoraAngles(request) => {
|
||||
// Webhook-required endpoint.
|
||||
let fal_client = client.get_fal_client_ref()?;
|
||||
request.send(fal_client).await
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
|
||||
/// Angle models (qwen_edit_2511_angles, flux_2_lora_angles) require at least
|
||||
/// one input image — both v1 plans error out otherwise. Mirror that strict
|
||||
/// requirement here so v1↔v2 parity holds for the "no inputs" case.
|
||||
pub fn require_at_least_one_image_input(
|
||||
image_inputs: &Option<ImageListRef>,
|
||||
) -> Result<(), ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require exactly one input image".to_string(),
|
||||
})),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) if tokens.is_empty() => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Angle models require exactly one input image, got {}", tokens.len()),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::Urls(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => Ok(()),
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
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 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 tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_quality::CommonQuality;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
/// Build an `OmniGenImageCostAndGenerateRequest` from the builder. All Artcraft
|
||||
/// image models delegate to this helper — the omni-gen endpoint handles all
|
||||
/// model-specific transformations server-side, so the client request stays
|
||||
/// extremely lightweight.
|
||||
///
|
||||
/// Per-model build functions are expected to validate model-specific
|
||||
/// constraints (e.g. unsupported aspect ratios) BEFORE calling this helper.
|
||||
/// Validation that's identical across every Artcraft image model — batch count
|
||||
/// (1..=4) and image_inputs shape — lives here.
|
||||
pub fn build_artcraft_omni_image_request(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
model: CommonImageModelEnum,
|
||||
) -> Result<OmniGenImageCostAndGenerateRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
let image_batch_count = plan_batch_count(builder.image_batch_count, strategy)?;
|
||||
let image_media_tokens = resolve_image_list_ref(builder.image_inputs.clone())?;
|
||||
|
||||
let aspect_ratio = builder.aspect_ratio.map(to_aspect_ratio_enum);
|
||||
let resolution = builder.resolution.map(to_resolution_enum);
|
||||
let quality = builder.quality.map(to_quality_enum);
|
||||
let idempotency_token = builder.get_or_generate_idempotency_token();
|
||||
|
||||
Ok(OmniGenImageCostAndGenerateRequest {
|
||||
idempotency_token: Some(idempotency_token),
|
||||
model: Some(model),
|
||||
prompt: builder.prompt.clone(),
|
||||
image_media_tokens,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
quality,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
adjust_horizontal_angle: builder.horizontal_angle,
|
||||
adjust_vertical_angle: builder.vertical_angle,
|
||||
adjust_zoom: builder.zoom,
|
||||
})
|
||||
}
|
||||
|
||||
/// Validate batch count (1..=4). Mirrors the v1 plan-level validation that
|
||||
/// every Artcraft image model performs.
|
||||
pub fn plan_batch_count(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<u16, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1..=4 => Ok(count),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
RequestMismatchMitigationStrategy::PayMoreUpgrade
|
||||
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(4),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve `image_inputs` to a `Vec<MediaFileToken>` for the omni-gen request.
|
||||
///
|
||||
/// Mirrors v1's behavior: URLs are accepted and dropped, because the omni-gen
|
||||
/// distillation hydrates media tokens to URLs before the cost path runs.
|
||||
/// Cost only depends on `num_images` + mode (derived from `image_inputs`
|
||||
/// presence by callers), so URL-form inputs flow through cleanly.
|
||||
pub fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Option<Vec<MediaFileToken>>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
|
||||
Some(ImageListRef::Urls(_)) => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_aspect_ratio_enum(ar: CommonAspectRatio) -> CommonAspectRatioEnum {
|
||||
match ar {
|
||||
CommonAspectRatio::Auto => CommonAspectRatioEnum::Auto,
|
||||
CommonAspectRatio::Auto2k => CommonAspectRatioEnum::Auto2k,
|
||||
CommonAspectRatio::Auto3k => CommonAspectRatioEnum::Auto3k,
|
||||
CommonAspectRatio::Auto4k => CommonAspectRatioEnum::Auto4k,
|
||||
CommonAspectRatio::Square => CommonAspectRatioEnum::Square,
|
||||
CommonAspectRatio::SquareHd => CommonAspectRatioEnum::SquareHd,
|
||||
CommonAspectRatio::WideFourByThree => CommonAspectRatioEnum::WideFourByThree,
|
||||
CommonAspectRatio::WideFiveByFour => CommonAspectRatioEnum::WideFiveByFour,
|
||||
CommonAspectRatio::WideThreeByTwo => CommonAspectRatioEnum::WideThreeByTwo,
|
||||
CommonAspectRatio::WideSixteenByNine => CommonAspectRatioEnum::WideSixteenByNine,
|
||||
CommonAspectRatio::WideTwentyOneByNine => CommonAspectRatioEnum::WideTwentyOneByNine,
|
||||
CommonAspectRatio::Wide => CommonAspectRatioEnum::Wide,
|
||||
CommonAspectRatio::TallThreeByFour => CommonAspectRatioEnum::TallThreeByFour,
|
||||
CommonAspectRatio::TallFourByFive => CommonAspectRatioEnum::TallFourByFive,
|
||||
CommonAspectRatio::TallTwoByThree => CommonAspectRatioEnum::TallTwoByThree,
|
||||
CommonAspectRatio::TallNineBySixteen => CommonAspectRatioEnum::TallNineBySixteen,
|
||||
CommonAspectRatio::TallNineByTwentyOne => CommonAspectRatioEnum::TallNineByTwentyOne,
|
||||
CommonAspectRatio::Tall => CommonAspectRatioEnum::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_resolution_enum(r: CommonResolution) -> CommonResolutionEnum {
|
||||
match r {
|
||||
CommonResolution::HalfK => CommonResolutionEnum::HalfK,
|
||||
CommonResolution::OneK => CommonResolutionEnum::OneK,
|
||||
CommonResolution::TwoK => CommonResolutionEnum::TwoK,
|
||||
CommonResolution::ThreeK => CommonResolutionEnum::ThreeK,
|
||||
CommonResolution::FourK => CommonResolutionEnum::FourK,
|
||||
CommonResolution::FourEightyP => CommonResolutionEnum::FourEightyP,
|
||||
CommonResolution::SevenTwentyP => CommonResolutionEnum::SevenTwentyP,
|
||||
CommonResolution::TenEightyP => CommonResolutionEnum::TenEightyP,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_quality_enum(q: CommonQuality) -> CommonQualityEnum {
|
||||
match q {
|
||||
CommonQuality::Low => CommonQualityEnum::Low,
|
||||
CommonQuality::Medium => CommonQualityEnum::Medium,
|
||||
CommonQuality::High => CommonQualityEnum::High,
|
||||
}
|
||||
}
|
||||
+318
@@ -0,0 +1,318 @@
|
||||
//! Strict combinatorial cost-parity tests between v1 (`build().estimate_costs()`)
|
||||
//! and v2 (`build2().estimate_cost()`) for every Artcraft image model.
|
||||
//!
|
||||
//! All sweeps use `PayMoreUpgrade` so v1 falls back to nearest-supported
|
||||
//! options instead of erroring out — this lets us assert strict `v1 == v2`
|
||||
//! parity across the full input domain. Both cents and credits are checked.
|
||||
//! Tests cover text-to-image AND image-to-image paths.
|
||||
//!
|
||||
//! If a sweep starts failing: don't `#[ignore]`. Either fix the v2 cost
|
||||
//! calculator to match v1, or fix the v1 plan, until both agree.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
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;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
// ── Shared sweep dimensions ────────────────────────────────────────────────
|
||||
|
||||
const BATCHES: &[Option<u16>] = &[
|
||||
None,
|
||||
Some(1),
|
||||
Some(2),
|
||||
Some(3),
|
||||
Some(4),
|
||||
];
|
||||
|
||||
fn all_aspect_ratios() -> &'static [Option<CommonAspectRatio>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::Auto2k),
|
||||
Some(CommonAspectRatio::Auto3k),
|
||||
Some(CommonAspectRatio::Auto4k),
|
||||
Some(CommonAspectRatio::Square),
|
||||
Some(CommonAspectRatio::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree),
|
||||
Some(CommonAspectRatio::WideFiveByFour),
|
||||
Some(CommonAspectRatio::WideThreeByTwo),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
Some(CommonAspectRatio::Wide),
|
||||
Some(CommonAspectRatio::TallThreeByFour),
|
||||
Some(CommonAspectRatio::TallFourByFive),
|
||||
Some(CommonAspectRatio::TallTwoByThree),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne),
|
||||
Some(CommonAspectRatio::Tall),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_resolutions() -> &'static [Option<CommonResolution>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonResolution::HalfK),
|
||||
Some(CommonResolution::OneK),
|
||||
Some(CommonResolution::TwoK),
|
||||
Some(CommonResolution::ThreeK),
|
||||
Some(CommonResolution::FourK),
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_qualities() -> &'static [Option<CommonQuality>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonQuality::Low),
|
||||
Some(CommonQuality::Medium),
|
||||
Some(CommonQuality::High),
|
||||
]
|
||||
}
|
||||
|
||||
/// MediaFileTokens at lengths 0/1/2/3 for sweeping the input-count axis.
|
||||
/// `None` is the text-to-image path; tokens are the image-to-image path.
|
||||
/// (Artcraft accepts only MediaFileTokens — URL form is rejected.)
|
||||
fn t2i_and_i2i_inputs() -> Vec<Option<ImageListRef>> {
|
||||
vec![
|
||||
None,
|
||||
Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
Some(ImageListRef::MediaFileTokens(vec![token(0)])),
|
||||
Some(ImageListRef::MediaFileTokens(vec![token(0), token(1)])),
|
||||
Some(ImageListRef::MediaFileTokens(vec![token(0), token(1), token(2)])),
|
||||
]
|
||||
}
|
||||
|
||||
/// Angle models reject empty/missing inputs in both v1 and v2 — restrict the
|
||||
/// sweep to non-empty MediaFileTokens shapes that both pipelines accept.
|
||||
fn angle_inputs() -> Vec<Option<ImageListRef>> {
|
||||
vec![
|
||||
Some(ImageListRef::MediaFileTokens(vec![token(0)])),
|
||||
Some(ImageListRef::MediaFileTokens(vec![token(0), token(1)])),
|
||||
Some(ImageListRef::MediaFileTokens(vec![token(0), token(1), token(2)])),
|
||||
]
|
||||
}
|
||||
|
||||
fn token(i: usize) -> MediaFileToken {
|
||||
MediaFileToken::new_from_str(&format!("mf_test{:028}", i))
|
||||
}
|
||||
|
||||
// ── Builder + parity helpers ───────────────────────────────────────────────
|
||||
|
||||
fn base_builder(model: CommonImageModel) -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
// PayMoreUpgrade so v1 gracefully degrades on unsupported options instead
|
||||
// of erroring — strict parity holds across the full sweep.
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_strict_cost_parity(builder: &GenerateImageRequestBuilder, msg: &str) {
|
||||
let v1_plan = builder.build();
|
||||
let v2_dor = builder.clone().build2();
|
||||
|
||||
match (&v1_plan, &v2_dor) {
|
||||
(Ok(_), Ok(_)) => {}
|
||||
(Err(_), Err(_)) => return,
|
||||
(Ok(_), Err(e)) => panic!("{} — v1 succeeded but v2 errored: {:?}", msg, e),
|
||||
(Err(e), Ok(_)) => panic!("{} — v2 succeeded but v1 errored: {:?}", msg, e),
|
||||
}
|
||||
let v1_estimate = v1_plan.unwrap().estimate_costs();
|
||||
let v2_estimate = v2_dor.unwrap().estimate_cost().expect("v2 estimate_cost should succeed");
|
||||
|
||||
assert_eq!(
|
||||
v2_estimate.cost_in_usd_cents, v1_estimate.cost_in_usd_cents,
|
||||
"{} — cost_in_usd_cents mismatch (v1={:?}, v2={:?})",
|
||||
msg, v1_estimate.cost_in_usd_cents, v2_estimate.cost_in_usd_cents,
|
||||
);
|
||||
assert_eq!(
|
||||
v2_estimate.cost_in_credits, v1_estimate.cost_in_credits,
|
||||
"{} — cost_in_credits mismatch (v1={:?}, v2={:?})",
|
||||
msg, v1_estimate.cost_in_credits, v2_estimate.cost_in_credits,
|
||||
);
|
||||
assert_eq!(
|
||||
v2_estimate.is_free, v1_estimate.is_free,
|
||||
"{} — is_free mismatch (v1={}, v2={})",
|
||||
msg, v1_estimate.is_free, v2_estimate.is_free,
|
||||
);
|
||||
assert_eq!(
|
||||
v2_estimate.is_unlimited, v1_estimate.is_unlimited,
|
||||
"{} — is_unlimited mismatch (v1={}, v2={})",
|
||||
msg, v1_estimate.is_unlimited, v2_estimate.is_unlimited,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Flat-priced (aspect × batch × inputs) ──────────────────────────────────
|
||||
|
||||
mod flat_priced_tests {
|
||||
use super::*;
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for image_inputs in &t2i_and_i2i_inputs() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
image_inputs: image_inputs.clone(),
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} ar={:?} batch={:?} inputs={:?}",
|
||||
model, aspect_ratio, batch, image_inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn flux_1_dev_parity() { sweep(CommonImageModel::Flux1Dev); }
|
||||
#[test] fn flux_1_schnell_parity() { sweep(CommonImageModel::Flux1Schnell); }
|
||||
#[test] fn flux_pro_1p1_parity() { sweep(CommonImageModel::FluxPro11); }
|
||||
#[test] fn flux_pro_1p1_ultra_parity() { sweep(CommonImageModel::FluxPro11Ultra); }
|
||||
#[test] fn nano_banana_parity() { sweep(CommonImageModel::NanoBanana); }
|
||||
#[test] fn seedream_4_parity() { sweep(CommonImageModel::Seedream4); }
|
||||
#[test] fn seedream_4p5_parity() { sweep(CommonImageModel::Seedream4p5); }
|
||||
#[test] fn seedream_5_lite_aspect_parity() { sweep(CommonImageModel::Seedream5Lite); }
|
||||
}
|
||||
|
||||
// ── Resolution-priced (aspect × batch × resolution × inputs) ───────────────
|
||||
|
||||
mod resolution_priced_tests {
|
||||
use super::*;
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for resolution in all_resolutions() {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for image_inputs in &t2i_and_i2i_inputs() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
resolution: *resolution,
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
image_inputs: image_inputs.clone(),
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} res={:?} ar={:?} batch={:?} inputs={:?}",
|
||||
model, resolution, aspect_ratio, batch, image_inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn nano_banana_2_parity() { sweep(CommonImageModel::NanoBanana2); }
|
||||
#[test] fn nano_banana_pro_parity() { sweep(CommonImageModel::NanoBananaPro); }
|
||||
|
||||
// Seedream 5 Lite cost is num_images-only, but the build path varies by
|
||||
// resolution (Auto2k vs Auto3k fallback for the bare-Auto case). Sweep
|
||||
// resolution too to make sure neither side derives a stray cost from it.
|
||||
#[test] fn seedream_5_lite_resolution_parity() { sweep(CommonImageModel::Seedream5Lite); }
|
||||
}
|
||||
|
||||
// ── Quality+size matrix (aspect × quality × batch × inputs) ────────────────
|
||||
|
||||
mod quality_priced_tests {
|
||||
use super::*;
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for quality in all_qualities() {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for image_inputs in &t2i_and_i2i_inputs() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
quality: *quality,
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
image_inputs: image_inputs.clone(),
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} q={:?} ar={:?} batch={:?} inputs={:?}",
|
||||
model, quality, aspect_ratio, batch, image_inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn gpt_image_1_parity() { sweep(CommonImageModel::GptImage1); }
|
||||
#[test] fn gpt_image_1p5_parity() { sweep(CommonImageModel::GptImage1p5); }
|
||||
#[test] fn gpt_image_2_parity() { sweep(CommonImageModel::GptImage2); }
|
||||
}
|
||||
|
||||
// ── Angle models (require ≥1 input; sweep angles × aspect × batch) ─────────
|
||||
|
||||
mod angle_tests {
|
||||
use super::*;
|
||||
|
||||
const ANGLES: &[Option<f64>] = &[None, Some(-45.0), Some(0.0), Some(45.0)];
|
||||
const ZOOMS: &[Option<f64>] = &[None, Some(0.0), Some(2.0)];
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for inputs in &angle_inputs() {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for h in ANGLES {
|
||||
for v in ANGLES {
|
||||
for z in ZOOMS {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: inputs.clone(),
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
horizontal_angle: *h,
|
||||
vertical_angle: *v,
|
||||
zoom: *z,
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} ar={:?} batch={:?} h={:?} v={:?} z={:?} inputs={:?}",
|
||||
model, aspect_ratio, batch, h, v, z, inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn qwen_edit_2511_angles_parity() { sweep(CommonImageModel::QwenEdit2511Angles); }
|
||||
#[test] fn flux_2_lora_angles_parity() { sweep(CommonImageModel::Flux2LoraAngles); }
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
|
||||
|
||||
pub fn build_artcraft_flux_1_dev(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Flux1Dev)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftFlux1Dev(ArtcraftFlux1DevRequestState { request }),
|
||||
))
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux 1 Dev. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_1_dev`): flat 2¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux1DevCostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux1DevCostState {
|
||||
pub fn from_request(request: &ArtcraftFlux1DevRequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.02/image. 1 credit = 1 USD cent.
|
||||
let cost_in_usd_cents = 2u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Flux1Dev,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_2_cents() { assert_eq!(cost_cents(1), 2); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_4_cents() { assert_eq!(cost_cents(2), 4); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_6_cents() { assert_eq!(cost_cents(3), 6); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_8_cents() { assert_eq!(cost_cents(4), 8); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux1DevRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux1DevRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
|
||||
|
||||
pub fn build_artcraft_flux_1_schnell(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Flux1Schnell)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftFlux1Schnell(ArtcraftFlux1SchnellRequestState { request }),
|
||||
))
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux 1 Schnell. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_1_schnell`): free, no per-image cost,
|
||||
/// rate-limited by Artcraft.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux1SchnellCostState;
|
||||
|
||||
impl ArtcraftFlux1SchnellCostState {
|
||||
pub fn from_request(_request: &ArtcraftFlux1SchnellRequestState) -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
cost_in_usd_cents: None,
|
||||
is_free: true,
|
||||
is_unlimited: true,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn build_estimate() -> crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Flux1Schnell,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_is_none() {
|
||||
let estimate = build_estimate();
|
||||
assert!(estimate.cost_in_usd_cents.is_none());
|
||||
assert!(estimate.cost_in_credits.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_free_is_true() {
|
||||
assert!(build_estimate().is_free);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_unlimited_is_true() {
|
||||
assert!(build_estimate().is_unlimited);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux1SchnellRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux1SchnellRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::angle_validation::require_at_least_one_image_input;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
|
||||
|
||||
pub fn build_artcraft_flux_2_lora_angles(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
require_at_least_one_image_input(&builder.image_inputs)?;
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Flux2LoraAngles)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftFlux2LoraAngles(ArtcraftFlux2LoraAnglesRequestState { request }),
|
||||
))
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux 2 LoRA Angles. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_2_lora_angles`): 4¢ per output image
|
||||
/// (Artcraft markup over Fal's 2¢/image base rate).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux2LoraAnglesCostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux2LoraAnglesCostState {
|
||||
pub fn from_request(request: &ArtcraftFlux2LoraAnglesRequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
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_builder::GenerateImageRequestBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new_from_str("mf_test")])),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-15.0),
|
||||
zoom: Some(2.0),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_8_cents() { assert_eq!(cost_cents(2), 8); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_12_cents() { assert_eq!(cost_cents(3), 12); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFlux2LoraAnglesRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftFlux2LoraAnglesRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
|
||||
|
||||
pub fn build_artcraft_flux_pro_1p1(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::FluxPro11)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftFluxPro1p1(ArtcraftFluxPro1p1RequestState { request }),
|
||||
))
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux Pro 1.1. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_pro_1p1`): 4¢ per output image (Fal's
|
||||
/// $0.04/MP rate at the default ~1MP).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFluxPro1p1CostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftFluxPro1p1CostState {
|
||||
pub fn from_request(request: &ArtcraftFluxPro1p1RequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::FluxPro11,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_8_cents() { assert_eq!(cost_cents(2), 8); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_12_cents() { assert_eq!(cost_cents(3), 12); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFluxPro1p1RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftFluxPro1p1RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
|
||||
|
||||
pub fn build_artcraft_flux_pro_1p1_ultra(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::FluxPro11Ultra)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftFluxPro1p1Ultra(ArtcraftFluxPro1p1UltraRequestState { request }),
|
||||
))
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
|
||||
|
||||
/// Cost state for Artcraft Flux Pro 1.1 Ultra. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_flux_pro_1p1_ultra`): 6¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFluxPro1p1UltraCostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftFluxPro1p1UltraCostState {
|
||||
pub fn from_request(request: &ArtcraftFluxPro1p1UltraRequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 6u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::FluxPro11Ultra,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_6_cents() { assert_eq!(cost_cents(1), 6); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_12_cents() { assert_eq!(cost_cents(2), 12); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_18_cents() { assert_eq!(cost_cents(3), 18); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_24_cents() { assert_eq!(cost_cents(4), 24); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftFluxPro1p1UltraRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftFluxPro1p1UltraRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
|
||||
|
||||
pub fn build_artcraft_gpt_image_1(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::GptImage1)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftGptImage1(ArtcraftGptImage1RequestState { request }),
|
||||
))
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
|
||||
|
||||
/// Cost state for Artcraft GPT Image 1. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_gpt_image_1`):
|
||||
///
|
||||
/// Output image cost (per output image):
|
||||
/// Low: 2¢ all sizes
|
||||
/// Medium: 5¢ square, 7¢ wide/tall
|
||||
/// High: 17¢ square, 25¢ wide/tall
|
||||
///
|
||||
/// Plus 2¢ per input image (high-fidelity estimate) in edit mode.
|
||||
/// Quality defaults to High when unspecified.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGptImage1CostState {
|
||||
pub quality: Option<CommonQualityEnum>,
|
||||
pub aspect_ratio: Option<CommonAspectRatioEnum>,
|
||||
pub num_images: u16,
|
||||
pub num_input_images: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage1CostState {
|
||||
pub fn from_request(request: &ArtcraftGptImage1RequestState) -> Self {
|
||||
Self {
|
||||
quality: request.request.quality,
|
||||
aspect_ratio: request.request.aspect_ratio,
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
num_input_images: request.request.image_media_tokens
|
||||
.as_ref()
|
||||
.map(|t| t.len() as u64)
|
||||
.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
||||
let is_square = is_square_or_none(self.aspect_ratio);
|
||||
|
||||
let output_cost_per_image: u64 = match (quality, is_square) {
|
||||
(CommonQualityEnum::Low, _) => 2,
|
||||
(CommonQualityEnum::Medium, true) => 5,
|
||||
(CommonQualityEnum::Medium, false) => 7,
|
||||
(CommonQualityEnum::High, true) => 17,
|
||||
(CommonQualityEnum::High, false) => 25,
|
||||
};
|
||||
|
||||
let output_cost = output_cost_per_image * self.num_images as u64;
|
||||
let input_image_cost = 2 * self.num_input_images;
|
||||
let cost_in_usd_cents = output_cost + input_image_cost;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_square_or_none(aspect_ratio: Option<CommonAspectRatioEnum>) -> bool {
|
||||
use CommonAspectRatioEnum as Ar;
|
||||
matches!(
|
||||
aspect_ratio,
|
||||
None
|
||||
| Some(Ar::Auto)
|
||||
| Some(Ar::Auto2k)
|
||||
| Some(Ar::Auto3k)
|
||||
| Some(Ar::Auto4k)
|
||||
| Some(Ar::Square)
|
||||
| Some(Ar::SquareHd)
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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_builder::GenerateImageRequestBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn base() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage1,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(1),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_text(quality: Option<CommonQuality>, aspect_ratio: Option<CommonAspectRatio>, batch: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
..base()
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
fn cost_edit(quality: Option<CommonQuality>, aspect_ratio: Option<CommonAspectRatio>, batch: u16, num_inputs: usize) -> u64 {
|
||||
let tokens: Vec<MediaFileToken> = (0..num_inputs)
|
||||
.map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i)))
|
||||
.collect();
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
quality,
|
||||
aspect_ratio,
|
||||
image_batch_count: Some(batch),
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(tokens)),
|
||||
..base()
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default (High square) → 17¢ ───────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_one_image_is_17c() { assert_eq!(cost_text(None, None, 1), 17); }
|
||||
|
||||
#[test]
|
||||
fn default_four_images_is_68c() { assert_eq!(cost_text(None, None, 4), 68); }
|
||||
|
||||
// ── Low → 2¢ all sizes ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one() { assert_eq!(cost_text(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 2); }
|
||||
|
||||
#[test]
|
||||
fn low_wide_one() { assert_eq!(cost_text(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2); }
|
||||
|
||||
#[test]
|
||||
fn low_tall_one() { assert_eq!(cost_text(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2); }
|
||||
|
||||
#[test]
|
||||
fn low_four_images() { assert_eq!(cost_text(Some(CommonQuality::Low), None, 4), 8); }
|
||||
|
||||
// ── Medium ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one() { assert_eq!(cost_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 5); }
|
||||
|
||||
#[test]
|
||||
fn medium_wide_one() { assert_eq!(cost_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 7); }
|
||||
|
||||
#[test]
|
||||
fn medium_tall_one() { assert_eq!(cost_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 7); }
|
||||
|
||||
// ── High ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one() { assert_eq!(cost_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17); }
|
||||
|
||||
#[test]
|
||||
fn high_wide_one() { assert_eq!(cost_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 25); }
|
||||
|
||||
#[test]
|
||||
fn high_wide_four() { assert_eq!(cost_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 100); }
|
||||
|
||||
// ── Edit mode adds 2¢ per input image ─────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn edit_one_input_adds_2c() {
|
||||
assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 1), 19);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_three_inputs_adds_6c() {
|
||||
assert_eq!(cost_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 3), 23);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_five_inputs_adds_10c() {
|
||||
assert_eq!(cost_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 2, 5), 24);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGptImage1RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage1RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
|
||||
|
||||
pub fn build_artcraft_gpt_image_1p5(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::GptImage1p5)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftGptImage1p5(ArtcraftGptImage1p5RequestState { request }),
|
||||
))
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
|
||||
|
||||
/// Cost state for Artcraft GPT Image 1.5. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_gpt_image_1p5`):
|
||||
///
|
||||
/// Output image cost (per output image):
|
||||
/// Low: 1¢ square, 2¢ wide/tall
|
||||
/// Medium: 4¢ square, 5¢ wide, 6¢ tall
|
||||
/// High: 14¢ square, 20¢ wide/tall
|
||||
///
|
||||
/// Quality defaults to High when unspecified.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGptImage1p5CostState {
|
||||
pub quality: Option<CommonQualityEnum>,
|
||||
pub aspect_ratio: Option<CommonAspectRatioEnum>,
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SizeBucket {
|
||||
Square,
|
||||
Wide,
|
||||
Tall,
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage1p5CostState {
|
||||
pub fn from_request(request: &ArtcraftGptImage1p5RequestState) -> Self {
|
||||
Self {
|
||||
quality: request.request.quality,
|
||||
aspect_ratio: request.request.aspect_ratio,
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
||||
let size = size_bucket(self.aspect_ratio);
|
||||
|
||||
let cost_per_image: u64 = match (quality, size) {
|
||||
(CommonQualityEnum::Low, SizeBucket::Square) => 1,
|
||||
(CommonQualityEnum::Low, _) => 2,
|
||||
|
||||
(CommonQualityEnum::Medium, SizeBucket::Square) => 4,
|
||||
(CommonQualityEnum::Medium, SizeBucket::Wide) => 5,
|
||||
(CommonQualityEnum::Medium, SizeBucket::Tall) => 6,
|
||||
|
||||
(CommonQualityEnum::High, SizeBucket::Square) => 14,
|
||||
(CommonQualityEnum::High, _) => 20,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
|
||||
use CommonAspectRatioEnum as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(Ar::Auto)
|
||||
| Some(Ar::Auto2k)
|
||||
| Some(Ar::Auto3k)
|
||||
| Some(Ar::Auto4k)
|
||||
| Some(Ar::Square)
|
||||
| Some(Ar::SquareHd) => SizeBucket::Square,
|
||||
|
||||
Some(Ar::WideThreeByTwo)
|
||||
| Some(Ar::WideFourByThree)
|
||||
| Some(Ar::WideFiveByFour)
|
||||
| Some(Ar::WideSixteenByNine)
|
||||
| Some(Ar::WideTwentyOneByNine)
|
||||
| Some(Ar::Wide) => SizeBucket::Wide,
|
||||
|
||||
Some(Ar::TallTwoByThree)
|
||||
| Some(Ar::TallThreeByFour)
|
||||
| Some(Ar::TallFourByFive)
|
||||
| Some(Ar::TallNineBySixteen)
|
||||
| Some(Ar::TallNineByTwentyOne)
|
||||
| Some(Ar::Tall) => SizeBucket::Tall,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(quality: Option<CommonQuality>, aspect_ratio: Option<CommonAspectRatio>, batch: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage1p5,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality,
|
||||
image_batch_count: Some(batch),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default (None → High) ─────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_square_one() { assert_eq!(cost_cents(None, Some(CommonAspectRatio::Square), 1), 14); }
|
||||
|
||||
#[test]
|
||||
fn default_unset_one() { assert_eq!(cost_cents(None, None, 1), 14); }
|
||||
|
||||
#[test]
|
||||
fn default_wide_one() { assert_eq!(cost_cents(None, Some(CommonAspectRatio::WideSixteenByNine), 1), 20); }
|
||||
|
||||
#[test]
|
||||
fn default_tall_one() { assert_eq!(cost_cents(None, Some(CommonAspectRatio::TallNineBySixteen), 1), 20); }
|
||||
|
||||
#[test]
|
||||
fn default_square_four() { assert_eq!(cost_cents(None, Some(CommonAspectRatio::Square), 4), 56); }
|
||||
|
||||
// ── Low ───────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square_one() { assert_eq!(cost_cents(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 1); }
|
||||
|
||||
#[test]
|
||||
fn low_wide_one() { assert_eq!(cost_cents(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2); }
|
||||
|
||||
#[test]
|
||||
fn low_tall_one() { assert_eq!(cost_cents(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2); }
|
||||
|
||||
// ── Medium ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_one() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 4); }
|
||||
|
||||
#[test]
|
||||
fn medium_wide_one() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 5); }
|
||||
|
||||
#[test]
|
||||
fn medium_tall_one() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 6); }
|
||||
|
||||
#[test]
|
||||
fn medium_tall_four() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 4), 24); }
|
||||
|
||||
// ── High ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_one() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 14); }
|
||||
|
||||
#[test]
|
||||
fn high_wide_four() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 80); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGptImage1p5RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage1p5RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
|
||||
|
||||
pub fn build_artcraft_gpt_image_2(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::GptImage2)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftGptImage2(ArtcraftGptImage2RequestState { request }),
|
||||
))
|
||||
}
|
||||
+201
@@ -0,0 +1,201 @@
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio as CommonAspectRatioEnum;
|
||||
use enums::common::generation::common_quality::CommonQuality as CommonQualityEnum;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
|
||||
|
||||
/// Cost state for Artcraft GPT Image 2. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_gpt_image_2`):
|
||||
///
|
||||
/// Output image cost (per output image), rounded up to whole cents:
|
||||
/// Low: 1¢ all sizes
|
||||
/// Medium: 4¢ landscape/portrait 4:3 or 16:9; 6¢ square or auto
|
||||
/// High: 15¢ landscape/portrait 4:3; 16¢ landscape/portrait 16:9;
|
||||
/// 22¢ square; 23¢ square_hd or auto
|
||||
///
|
||||
/// Quality defaults to High when unspecified.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGptImage2CostState {
|
||||
pub quality: Option<CommonQualityEnum>,
|
||||
pub aspect_ratio: Option<CommonAspectRatioEnum>,
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum SizeBucket {
|
||||
Square,
|
||||
SquareHd,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage2CostState {
|
||||
pub fn from_request(request: &ArtcraftGptImage2RequestState) -> Self {
|
||||
Self {
|
||||
quality: request.request.quality,
|
||||
aspect_ratio: request.request.aspect_ratio,
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let quality = self.quality.unwrap_or(CommonQualityEnum::High);
|
||||
let size = size_bucket(self.aspect_ratio);
|
||||
|
||||
let cost_per_image: u64 = match (quality, size) {
|
||||
(CommonQualityEnum::Low, _) => 1,
|
||||
|
||||
(CommonQualityEnum::Medium, SizeBucket::Square)
|
||||
| (CommonQualityEnum::Medium, SizeBucket::SquareHd)
|
||||
| (CommonQualityEnum::Medium, SizeBucket::Auto) => 6,
|
||||
(CommonQualityEnum::Medium, SizeBucket::Landscape4x3)
|
||||
| (CommonQualityEnum::Medium, SizeBucket::Portrait4x3)
|
||||
| (CommonQualityEnum::Medium, SizeBucket::Landscape16x9)
|
||||
| (CommonQualityEnum::Medium, SizeBucket::Portrait16x9) => 4,
|
||||
|
||||
(CommonQualityEnum::High, SizeBucket::Square) => 22,
|
||||
(CommonQualityEnum::High, SizeBucket::SquareHd) | (CommonQualityEnum::High, SizeBucket::Auto) => 23,
|
||||
(CommonQualityEnum::High, SizeBucket::Landscape4x3)
|
||||
| (CommonQualityEnum::High, SizeBucket::Portrait4x3) => 15,
|
||||
(CommonQualityEnum::High, SizeBucket::Landscape16x9)
|
||||
| (CommonQualityEnum::High, SizeBucket::Portrait16x9) => 16,
|
||||
};
|
||||
|
||||
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors v1's `plan_image_size` for GPT Image 2 — see the v1 file
|
||||
/// `plan_generate_image_artcraft_gpt_image_2.rs::plan_image_size`.
|
||||
fn size_bucket(aspect_ratio: Option<CommonAspectRatioEnum>) -> SizeBucket {
|
||||
use CommonAspectRatioEnum as Ar;
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(Ar::Auto)
|
||||
| Some(Ar::Auto2k)
|
||||
| Some(Ar::Auto3k)
|
||||
| Some(Ar::Auto4k) => SizeBucket::Auto,
|
||||
|
||||
Some(Ar::Square) => SizeBucket::Square,
|
||||
Some(Ar::SquareHd) => SizeBucket::SquareHd,
|
||||
|
||||
Some(Ar::WideFourByThree) | Some(Ar::WideFiveByFour) => SizeBucket::Landscape4x3,
|
||||
|
||||
Some(Ar::WideThreeByTwo)
|
||||
| Some(Ar::WideSixteenByNine)
|
||||
| Some(Ar::WideTwentyOneByNine)
|
||||
| Some(Ar::Wide) => SizeBucket::Landscape16x9,
|
||||
|
||||
Some(Ar::TallThreeByFour) | Some(Ar::TallFourByFive) => SizeBucket::Portrait4x3,
|
||||
|
||||
Some(Ar::TallTwoByThree)
|
||||
| Some(Ar::TallNineBySixteen)
|
||||
| Some(Ar::TallNineByTwentyOne)
|
||||
| Some(Ar::Tall) => SizeBucket::Portrait16x9,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
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::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(quality: Option<CommonQuality>, aspect_ratio: Option<CommonAspectRatio>, batch: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::GptImage2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio,
|
||||
quality,
|
||||
image_batch_count: Some(batch),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Low (1¢ all sizes) ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn low_square() { assert_eq!(cost_cents(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 1); }
|
||||
|
||||
#[test]
|
||||
fn low_square_hd() { assert_eq!(cost_cents(Some(CommonQuality::Low), Some(CommonAspectRatio::SquareHd), 1), 1); }
|
||||
|
||||
#[test]
|
||||
fn low_landscape_4x3() { assert_eq!(cost_cents(Some(CommonQuality::Low), Some(CommonAspectRatio::WideFourByThree), 1), 1); }
|
||||
|
||||
#[test]
|
||||
fn low_four() { assert_eq!(cost_cents(Some(CommonQuality::Low), None, 4), 4); }
|
||||
|
||||
// ── Medium ────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn medium_square_is_6c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 6); }
|
||||
|
||||
#[test]
|
||||
fn medium_square_hd_is_6c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::SquareHd), 1), 6); }
|
||||
|
||||
#[test]
|
||||
fn medium_auto_is_6c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), None, 1), 6); }
|
||||
|
||||
#[test]
|
||||
fn medium_landscape_4x3_is_4c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideFourByThree), 1), 4); }
|
||||
|
||||
#[test]
|
||||
fn medium_portrait_4x3_is_4c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallThreeByFour), 1), 4); }
|
||||
|
||||
#[test]
|
||||
fn medium_landscape_16x9_is_4c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 4); }
|
||||
|
||||
#[test]
|
||||
fn medium_portrait_16x9_is_4c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 4); }
|
||||
|
||||
#[test]
|
||||
fn medium_square_four_is_24c() { assert_eq!(cost_cents(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 24); }
|
||||
|
||||
// ── High ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn high_square_is_22c() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 22); }
|
||||
|
||||
#[test]
|
||||
fn high_square_hd_is_23c() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::SquareHd), 1), 23); }
|
||||
|
||||
#[test]
|
||||
fn high_auto_is_23c() { assert_eq!(cost_cents(Some(CommonQuality::High), None, 1), 23); }
|
||||
|
||||
#[test]
|
||||
fn high_landscape_4x3_is_15c() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::WideFourByThree), 1), 15); }
|
||||
|
||||
#[test]
|
||||
fn high_landscape_16x9_is_16c() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 16); }
|
||||
|
||||
#[test]
|
||||
fn high_square_four_is_88c() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 88); }
|
||||
|
||||
#[test]
|
||||
fn high_square_hd_four_is_92c() { assert_eq!(cost_cents(Some(CommonQuality::High), Some(CommonAspectRatio::SquareHd), 4), 92); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGptImage2RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftGptImage2RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
pub(crate) mod angle_validation;
|
||||
pub(crate) mod build_common;
|
||||
pub mod flux_1_dev;
|
||||
pub mod flux_1_schnell;
|
||||
pub mod flux_2_lora_angles;
|
||||
pub mod flux_pro_1p1;
|
||||
pub mod flux_pro_1p1_ultra;
|
||||
pub mod gpt_image_1;
|
||||
pub mod gpt_image_1p5;
|
||||
pub mod gpt_image_2;
|
||||
pub mod nano_banana;
|
||||
pub mod nano_banana_2;
|
||||
pub mod nano_banana_pro;
|
||||
pub mod qwen_edit_2511_angles;
|
||||
pub(crate) mod request_common;
|
||||
pub mod seedream_4;
|
||||
pub mod seedream_4p5;
|
||||
pub mod seedream_5_lite;
|
||||
|
||||
#[cfg(test)]
|
||||
mod cost_parity_tests;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
|
||||
|
||||
pub fn build_artcraft_nano_banana(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::NanoBanana)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftNanoBanana(ArtcraftNanoBananaRequestState { request }),
|
||||
))
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
|
||||
|
||||
/// Cost state for Artcraft Nano Banana (Gemini 2.5 Flash). Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_nano_banana`): 4¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftNanoBananaCostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftNanoBananaCostState {
|
||||
pub fn from_request(request: &ArtcraftNanoBananaRequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::NanoBanana,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_8_cents() { assert_eq!(cost_cents(2), 8); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_12_cents() { assert_eq!(cost_cents(3), 12); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftNanoBananaRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftNanoBananaRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::request::ArtcraftNanoBanana2RequestState;
|
||||
|
||||
pub fn build_artcraft_nano_banana_2(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::NanoBanana2)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftNanoBanana2(ArtcraftNanoBanana2RequestState { request }),
|
||||
))
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_2::request::ArtcraftNanoBanana2RequestState;
|
||||
|
||||
/// Cost state for Artcraft Nano Banana 2. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_nano_banana_2`):
|
||||
///
|
||||
/// 0.5K → 6¢, 1K (default) → 8¢, 2K → 12¢, 4K → 16¢. 3K falls back to 2K
|
||||
/// pricing; legacy video resolutions (480p/720p/1080p) fall back to 1K.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftNanoBanana2CostState {
|
||||
pub resolution: Option<CommonResolutionEnum>,
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftNanoBanana2CostState {
|
||||
pub fn from_request(request: &ArtcraftNanoBanana2RequestState) -> Self {
|
||||
Self {
|
||||
resolution: request.request.resolution,
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_per_image: u64 = cost_per_image_in_cents(self.resolution);
|
||||
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_per_image_in_cents(resolution: Option<CommonResolutionEnum>) -> u64 {
|
||||
use CommonResolutionEnum as R;
|
||||
match resolution {
|
||||
Some(R::HalfK) => 6,
|
||||
None
|
||||
| Some(R::OneK)
|
||||
| Some(R::FourEightyP)
|
||||
| Some(R::SevenTwentyP)
|
||||
| Some(R::TenEightyP) => 8,
|
||||
Some(R::TwoK) | Some(R::ThreeK) => 12,
|
||||
Some(R::FourK) => 16,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::NanoBanana2,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── Default / 1K (8¢) ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_resolution_one_image_is_8c() { assert_eq!(cost_cents(None, 1), 8); }
|
||||
|
||||
#[test]
|
||||
fn default_resolution_four_images_is_32c() { assert_eq!(cost_cents(None, 4), 32); }
|
||||
|
||||
#[test]
|
||||
fn one_k_one_image_is_8c() { assert_eq!(cost_cents(Some(CommonResolution::OneK), 1), 8); }
|
||||
|
||||
// ── 0.5K (6¢) ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn half_k_one_image_is_6c() { assert_eq!(cost_cents(Some(CommonResolution::HalfK), 1), 6); }
|
||||
|
||||
#[test]
|
||||
fn half_k_four_images_is_24c() { assert_eq!(cost_cents(Some(CommonResolution::HalfK), 4), 24); }
|
||||
|
||||
// ── 2K (12¢) ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn two_k_one_image_is_12c() { assert_eq!(cost_cents(Some(CommonResolution::TwoK), 1), 12); }
|
||||
|
||||
#[test]
|
||||
fn two_k_four_images_is_48c() { assert_eq!(cost_cents(Some(CommonResolution::TwoK), 4), 48); }
|
||||
|
||||
// ── 3K falls back to 2K pricing ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn three_k_one_image_falls_back_to_two_k_pricing() {
|
||||
assert_eq!(cost_cents(Some(CommonResolution::ThreeK), 1), 12);
|
||||
}
|
||||
|
||||
// ── 4K (16¢) ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn four_k_one_image_is_16c() { assert_eq!(cost_cents(Some(CommonResolution::FourK), 1), 16); }
|
||||
|
||||
#[test]
|
||||
fn four_k_four_images_is_64c() { assert_eq!(cost_cents(Some(CommonResolution::FourK), 4), 64); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftNanoBanana2RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftNanoBanana2RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::request::ArtcraftNanoBananaProRequestState;
|
||||
|
||||
pub fn build_artcraft_nano_banana_pro(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::NanoBananaPro)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftNanoBananaPro(ArtcraftNanoBananaProRequestState { request }),
|
||||
))
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::nano_banana_pro::request::ArtcraftNanoBananaProRequestState;
|
||||
|
||||
/// Cost state for Artcraft Nano Banana Pro. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_nano_banana_pro`):
|
||||
///
|
||||
/// ≤2K (default 1K) → 15¢, 4K → 30¢. 3K falls back to 2K pricing; legacy
|
||||
/// video resolutions and 0.5K fall back to 1K pricing.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftNanoBananaProCostState {
|
||||
pub resolution: Option<CommonResolutionEnum>,
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftNanoBananaProCostState {
|
||||
pub fn from_request(request: &ArtcraftNanoBananaProRequestState) -> Self {
|
||||
Self {
|
||||
resolution: request.request.resolution,
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_per_image: u64 = match self.resolution {
|
||||
Some(CommonResolutionEnum::FourK) => 30,
|
||||
_ => 15,
|
||||
};
|
||||
let cost_in_usd_cents = cost_per_image * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::NanoBananaPro,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
// ── ≤2K → 15¢/image ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_resolution_one_image_is_15c() { assert_eq!(cost_cents(None, 1), 15); }
|
||||
|
||||
#[test]
|
||||
fn one_k_one_image_is_15c() { assert_eq!(cost_cents(Some(CommonResolution::OneK), 1), 15); }
|
||||
|
||||
#[test]
|
||||
fn two_k_one_image_is_15c() { assert_eq!(cost_cents(Some(CommonResolution::TwoK), 1), 15); }
|
||||
|
||||
#[test]
|
||||
fn one_k_two_images_is_30c() { assert_eq!(cost_cents(Some(CommonResolution::OneK), 2), 30); }
|
||||
|
||||
#[test]
|
||||
fn one_k_four_images_is_60c() { assert_eq!(cost_cents(Some(CommonResolution::OneK), 4), 60); }
|
||||
|
||||
// ── 4K → 30¢/image ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn four_k_one_image_is_30c() { assert_eq!(cost_cents(Some(CommonResolution::FourK), 1), 30); }
|
||||
|
||||
#[test]
|
||||
fn four_k_two_images_is_60c() { assert_eq!(cost_cents(Some(CommonResolution::FourK), 2), 60); }
|
||||
|
||||
#[test]
|
||||
fn four_k_four_images_is_120c() { assert_eq!(cost_cents(Some(CommonResolution::FourK), 4), 120); }
|
||||
|
||||
// ── 3K and legacy resolutions fall back to 15¢ ─────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn three_k_falls_back_to_15c() { assert_eq!(cost_cents(Some(CommonResolution::ThreeK), 1), 15); }
|
||||
|
||||
#[test]
|
||||
fn half_k_falls_back_to_15c() { assert_eq!(cost_cents(Some(CommonResolution::HalfK), 1), 15); }
|
||||
|
||||
#[test]
|
||||
fn ten_eighty_p_falls_back_to_15c() { assert_eq!(cost_cents(Some(CommonResolution::TenEightyP), 1), 15); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftNanoBananaProRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftNanoBananaProRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::angle_validation::require_at_least_one_image_input;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::request::ArtcraftQwenEdit2511AnglesRequestState;
|
||||
|
||||
pub fn build_artcraft_qwen_edit_2511_angles(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
require_at_least_one_image_input(&builder.image_inputs)?;
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::QwenEdit2511Angles)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftQwenEdit2511Angles(ArtcraftQwenEdit2511AnglesRequestState { request }),
|
||||
))
|
||||
}
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::qwen_edit_2511_angles::request::ArtcraftQwenEdit2511AnglesRequestState;
|
||||
|
||||
/// Cost state for Artcraft Qwen Edit 2511 Angles. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_qwen_edit_2511_angles`): 4¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftQwenEdit2511AnglesCostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftQwenEdit2511AnglesCostState {
|
||||
pub fn from_request(request: &ArtcraftQwenEdit2511AnglesRequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
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_builder::GenerateImageRequestBuilder;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new_from_str("mf_test")])),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-15.0),
|
||||
zoom: Some(2.0),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_8_cents() { assert_eq!(cost_cents(2), 8); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_12_cents() { assert_eq!(cost_cents(3), 12); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftQwenEdit2511AnglesRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftQwenEdit2511AnglesRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_client::endpoints::omni_gen::generate::image::omni_gen_image::omni_gen_image_generate;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
ArtcraftImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
|
||||
/// Send an image generation request to the Artcraft omni-gen image endpoint.
|
||||
/// All Artcraft model request states delegate to this function.
|
||||
pub async fn send_artcraft_omni_image_request(
|
||||
request: &OmniGenImageCostAndGenerateRequest,
|
||||
client: &RouterArtcraftClient,
|
||||
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let response = omni_gen_image_generate(
|
||||
&client.api_host,
|
||||
Some(&client.credentials),
|
||||
request.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
|
||||
inference_job_token: response.inference_job_token,
|
||||
}))
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::request::ArtcraftSeedream4RequestState;
|
||||
|
||||
pub fn build_artcraft_seedream_4(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Seedream4)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftSeedream4(ArtcraftSeedream4RequestState { request }),
|
||||
))
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4::request::ArtcraftSeedream4RequestState;
|
||||
|
||||
/// Cost state for Artcraft Seedream V4. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_seedream_4`): 3¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedream4CostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedream4CostState {
|
||||
pub fn from_request(request: &ArtcraftSeedream4RequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 3u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Seedream4,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_3_cents() { assert_eq!(cost_cents(1), 3); }
|
||||
|
||||
#[test]
|
||||
fn two_images_costs_6_cents() { assert_eq!(cost_cents(2), 6); }
|
||||
|
||||
#[test]
|
||||
fn three_images_costs_9_cents() { assert_eq!(cost_cents(3), 9); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_12_cents() { assert_eq!(cost_cents(4), 12); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedream4RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedream4RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::request::ArtcraftSeedream4p5RequestState;
|
||||
|
||||
pub fn build_artcraft_seedream_4p5(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Seedream4p5)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftSeedream4p5(ArtcraftSeedream4p5RequestState { request }),
|
||||
))
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_4p5::request::ArtcraftSeedream4p5RequestState;
|
||||
|
||||
/// Cost state for Artcraft Seedream V4.5. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_seedream_4p5`): 4¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedream4p5CostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedream4p5CostState {
|
||||
pub fn from_request(request: &ArtcraftSeedream4p5RequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Seedream4p5,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedream4p5RequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedream4p5RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::build_common::build_artcraft_omni_image_request;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::request::ArtcraftSeedream5LiteRequestState;
|
||||
|
||||
pub fn build_artcraft_seedream_5_lite(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Seedream5Lite)?;
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::ArtcraftSeedream5Lite(ArtcraftSeedream5LiteRequestState { request }),
|
||||
))
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::seedream_5_lite::request::ArtcraftSeedream5LiteRequestState;
|
||||
|
||||
/// Cost state for Artcraft Seedream 5 Lite. Pricing mirrors v1
|
||||
/// (`estimate_image_cost_artcraft_seedream_5_lite`): 4¢ per output image.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedream5LiteCostState {
|
||||
pub num_images: u16,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedream5LiteCostState {
|
||||
pub fn from_request(request: &ArtcraftSeedream5LiteRequestState) -> Self {
|
||||
Self {
|
||||
num_images: request.request.image_batch_count.unwrap_or(1),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = 4u64 * self.num_images as u64;
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
fn cost_cents(image_batch_count: u16) -> u64 {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Seedream5Lite,
|
||||
provider: Provider::Artcraft,
|
||||
prompt: None,
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: Some(image_batch_count),
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
};
|
||||
builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_costs_4_cents() { assert_eq!(cost_cents(1), 4); }
|
||||
|
||||
#[test]
|
||||
fn four_images_costs_16_cents() { assert_eq!(cost_cents(4), 16); }
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
|
||||
use crate::generate::generate_image_v2::providers::artcraft::request_common::send_artcraft_omni_image_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftSeedream5LiteRequestState {
|
||||
pub request: OmniGenImageCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftSeedream5LiteRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_image_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
//! Strict combinatorial cost-parity tests between v1 (`build().estimate_costs()`)
|
||||
//! and v2 (`build2().estimate_cost()`) for every Fal image model that has a
|
||||
//! v1 plan.
|
||||
//!
|
||||
//! All sweeps use `PayMoreUpgrade` so v1 gracefully degrades on unsupported
|
||||
//! options instead of erroring — this lets us assert strict `v1 == v2`
|
||||
//! parity across the full input domain. Both cents and credits are checked.
|
||||
//! Tests cover text-to-image AND image-to-image paths.
|
||||
//!
|
||||
//! If a sweep starts failing: don't `#[ignore]`. Either fix the v2 cost
|
||||
//! calculator to match v1, or fix the v1 plan, until both agree.
|
||||
//!
|
||||
//! Note: Fal v2 models that have NO v1 plan (qwen_edit_2511_angles,
|
||||
//! flux_2_lora_angles) are excluded from this file — there is no v1 cost to
|
||||
//! compare against. Their per-module unit tests in `cost.rs` cover the v2
|
||||
//! pricing independently.
|
||||
|
||||
#![cfg(test)]
|
||||
|
||||
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;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
|
||||
// ── Shared sweep dimensions ────────────────────────────────────────────────
|
||||
|
||||
const BATCHES: &[Option<u16>] = &[
|
||||
None,
|
||||
Some(1),
|
||||
Some(2),
|
||||
Some(3),
|
||||
Some(4),
|
||||
];
|
||||
|
||||
fn all_aspect_ratios() -> &'static [Option<CommonAspectRatio>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::Auto2k),
|
||||
Some(CommonAspectRatio::Auto3k),
|
||||
Some(CommonAspectRatio::Auto4k),
|
||||
Some(CommonAspectRatio::Square),
|
||||
Some(CommonAspectRatio::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree),
|
||||
Some(CommonAspectRatio::WideFiveByFour),
|
||||
Some(CommonAspectRatio::WideThreeByTwo),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
Some(CommonAspectRatio::Wide),
|
||||
Some(CommonAspectRatio::TallThreeByFour),
|
||||
Some(CommonAspectRatio::TallFourByFive),
|
||||
Some(CommonAspectRatio::TallTwoByThree),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne),
|
||||
Some(CommonAspectRatio::Tall),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_resolutions() -> &'static [Option<CommonResolution>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonResolution::HalfK),
|
||||
Some(CommonResolution::OneK),
|
||||
Some(CommonResolution::TwoK),
|
||||
Some(CommonResolution::ThreeK),
|
||||
Some(CommonResolution::FourK),
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP),
|
||||
]
|
||||
}
|
||||
|
||||
fn all_qualities() -> &'static [Option<CommonQuality>] {
|
||||
&[
|
||||
None,
|
||||
Some(CommonQuality::Low),
|
||||
Some(CommonQuality::Medium),
|
||||
Some(CommonQuality::High),
|
||||
]
|
||||
}
|
||||
|
||||
/// URL-form inputs of varying lengths to sweep the image-to-image axis.
|
||||
/// `None` is the text-to-image path. (Fal accepts only Url-form inputs —
|
||||
/// MediaFileTokens cause `FalOnlySupportsUrls` on both pipelines, so they're
|
||||
/// out of scope here; we sweep only the meaningful URL combinations.)
|
||||
fn t2i_and_i2i_inputs() -> Vec<Option<ImageListRef>> {
|
||||
vec![
|
||||
None,
|
||||
Some(ImageListRef::Urls(vec![])),
|
||||
Some(ImageListRef::Urls(vec![url(0)])),
|
||||
Some(ImageListRef::Urls(vec![url(0), url(1)])),
|
||||
Some(ImageListRef::Urls(vec![url(0), url(1), url(2)])),
|
||||
]
|
||||
}
|
||||
|
||||
fn url(i: usize) -> String {
|
||||
format!("https://example.com/img_{}.jpg", i)
|
||||
}
|
||||
|
||||
// ── Builder + parity helpers ───────────────────────────────────────────────
|
||||
|
||||
fn base_builder(model: CommonImageModel) -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
// PayMoreUpgrade so v1 gracefully degrades on unsupported options instead
|
||||
// of erroring — strict parity holds across the full sweep.
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn assert_strict_cost_parity(builder: &GenerateImageRequestBuilder, msg: &str) {
|
||||
let v1_plan = builder.build();
|
||||
let v2_dor = builder.clone().build2();
|
||||
|
||||
match (&v1_plan, &v2_dor) {
|
||||
(Ok(_), Ok(_)) => {}
|
||||
(Err(_), Err(_)) => return,
|
||||
(Ok(_), Err(e)) => panic!("{} — v1 succeeded but v2 errored: {:?}", msg, e),
|
||||
(Err(e), Ok(_)) => panic!("{} — v2 succeeded but v1 errored: {:?}", msg, e),
|
||||
}
|
||||
let v1_estimate = v1_plan.unwrap().estimate_costs();
|
||||
let v2_estimate = v2_dor.unwrap().estimate_cost().expect("v2 estimate_cost should succeed");
|
||||
|
||||
assert_eq!(
|
||||
v2_estimate.cost_in_usd_cents, v1_estimate.cost_in_usd_cents,
|
||||
"{} — cost_in_usd_cents mismatch (v1={:?}, v2={:?})",
|
||||
msg, v1_estimate.cost_in_usd_cents, v2_estimate.cost_in_usd_cents,
|
||||
);
|
||||
assert_eq!(
|
||||
v2_estimate.cost_in_credits, v1_estimate.cost_in_credits,
|
||||
"{} — cost_in_credits mismatch (v1={:?}, v2={:?})",
|
||||
msg, v1_estimate.cost_in_credits, v2_estimate.cost_in_credits,
|
||||
);
|
||||
assert_eq!(
|
||||
v2_estimate.is_free, v1_estimate.is_free,
|
||||
"{} — is_free mismatch (v1={}, v2={})",
|
||||
msg, v1_estimate.is_free, v2_estimate.is_free,
|
||||
);
|
||||
assert_eq!(
|
||||
v2_estimate.is_unlimited, v1_estimate.is_unlimited,
|
||||
"{} — is_unlimited mismatch (v1={}, v2={})",
|
||||
msg, v1_estimate.is_unlimited, v2_estimate.is_unlimited,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Flat-priced (aspect × batch × inputs) ──────────────────────────────────
|
||||
|
||||
mod flat_priced_tests {
|
||||
use super::*;
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for image_inputs in &t2i_and_i2i_inputs() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
image_inputs: image_inputs.clone(),
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} ar={:?} batch={:?} inputs={:?}",
|
||||
model, aspect_ratio, batch, image_inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn flux_1_dev_parity() { sweep(CommonImageModel::Flux1Dev); }
|
||||
#[test] fn flux_1_schnell_parity() { sweep(CommonImageModel::Flux1Schnell); }
|
||||
#[test] fn flux_pro_1p1_parity() { sweep(CommonImageModel::FluxPro11); }
|
||||
#[test] fn flux_pro_1p1_ultra_parity() { sweep(CommonImageModel::FluxPro11Ultra); }
|
||||
#[test] fn nano_banana_parity() { sweep(CommonImageModel::NanoBanana); }
|
||||
#[test] fn seedream_4_parity() { sweep(CommonImageModel::Seedream4); }
|
||||
#[test] fn seedream_4p5_parity() { sweep(CommonImageModel::Seedream4p5); }
|
||||
#[test] fn seedream_5_lite_parity() { sweep(CommonImageModel::Seedream5Lite); }
|
||||
}
|
||||
|
||||
// ── Resolution-priced (aspect × batch × resolution × inputs) ───────────────
|
||||
|
||||
mod resolution_priced_tests {
|
||||
use super::*;
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for resolution in all_resolutions() {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for image_inputs in &t2i_and_i2i_inputs() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
resolution: *resolution,
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
image_inputs: image_inputs.clone(),
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} res={:?} ar={:?} batch={:?} inputs={:?}",
|
||||
model, resolution, aspect_ratio, batch, image_inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn nano_banana_2_parity() { sweep(CommonImageModel::NanoBanana2); }
|
||||
#[test] fn nano_banana_pro_parity() { sweep(CommonImageModel::NanoBananaPro); }
|
||||
}
|
||||
|
||||
// ── Quality+size matrix (aspect × quality × batch × inputs) ────────────────
|
||||
|
||||
mod quality_priced_tests {
|
||||
use super::*;
|
||||
|
||||
fn sweep(model: CommonImageModel) {
|
||||
for quality in all_qualities() {
|
||||
for aspect_ratio in all_aspect_ratios() {
|
||||
for batch in BATCHES {
|
||||
for image_inputs in &t2i_and_i2i_inputs() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
quality: *quality,
|
||||
aspect_ratio: *aspect_ratio,
|
||||
image_batch_count: *batch,
|
||||
image_inputs: image_inputs.clone(),
|
||||
..base_builder(model)
|
||||
};
|
||||
assert_strict_cost_parity(
|
||||
&builder,
|
||||
&format!(
|
||||
"{:?} q={:?} ar={:?} batch={:?} inputs={:?}",
|
||||
model, quality, aspect_ratio, batch, image_inputs,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test] fn gpt_image_1_parity() { sweep(CommonImageModel::GptImage1); }
|
||||
#[test] fn gpt_image_1p5_parity() { sweep(CommonImageModel::GptImage1p5); }
|
||||
#[test] fn gpt_image_2_parity() { sweep(CommonImageModel::GptImage2); }
|
||||
}
|
||||
+14
-6
@@ -120,15 +120,20 @@ fn plan_aspect_ratio(
|
||||
|
||||
// ── Image inputs ──
|
||||
|
||||
/// Flux 1 Dev edit takes a single image URL, not a list.
|
||||
/// If multiple URLs are provided, we take the first one.
|
||||
/// Flux 1 Dev edit takes exactly one image URL. v1 rejects multi-URL inputs;
|
||||
/// match that strictness so cost parity holds across the full sweep.
|
||||
fn resolve_single_image_url(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) if urls.is_empty() => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) if urls.len() == 1 => Ok(Some(urls.into_iter().next().unwrap())),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
Ok(urls.into_iter().next())
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Flux 1 Dev image-to-image supports exactly 1 image, got {}", urls.len()),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
@@ -205,7 +210,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_urls_takes_first() {
|
||||
fn multiple_urls_rejected_for_parity_with_v1() {
|
||||
// v1 hard-rejects >1 URL for Flux 1 Dev edit. v2 mirrors that.
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/a.jpg".to_string(),
|
||||
@@ -213,8 +219,10 @@ mod tests {
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_edit(build_fal_flux_1_dev(builder));
|
||||
assert_eq!(req.image_url, "https://example.com/a.jpg");
|
||||
assert!(matches!(
|
||||
build_fal_flux_1_dev(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+4
-3
@@ -18,12 +18,13 @@ impl FalFlux1DevCostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
@@ -107,6 +108,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
+16
-4
@@ -141,12 +141,21 @@ fn plan_edit_image_size(
|
||||
|
||||
// ── Image inputs ──
|
||||
|
||||
/// Flux 1 Schnell redux takes exactly one image URL. v1 rejects multi-URL
|
||||
/// inputs; match that strictness so cost parity holds across the full sweep.
|
||||
fn resolve_single_image_url(
|
||||
image_inputs: Option<ImageListRef>,
|
||||
) -> Result<Option<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls.into_iter().next()),
|
||||
Some(ImageListRef::Urls(urls)) if urls.is_empty() => Ok(None),
|
||||
Some(ImageListRef::Urls(urls)) if urls.len() == 1 => Ok(Some(urls.into_iter().next().unwrap())),
|
||||
Some(ImageListRef::Urls(urls)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: format!("Flux 1 Schnell redux supports exactly 1 image, got {}", urls.len()),
|
||||
}))
|
||||
}
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
@@ -222,7 +231,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_urls_takes_first() {
|
||||
fn multiple_urls_rejected_for_parity_with_v1() {
|
||||
// v1 hard-rejects >1 URL for Flux 1 Schnell redux. v2 mirrors that.
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/a.jpg".to_string(),
|
||||
@@ -230,8 +240,10 @@ mod tests {
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_edit(build_fal_flux_1_schnell(builder));
|
||||
assert_eq!(req.image_url, "https://example.com/a.jpg");
|
||||
assert!(matches!(
|
||||
build_fal_flux_1_schnell(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-2
@@ -18,7 +18,8 @@ impl FalFlux1SchnellCostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -108,6 +109,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
+184
@@ -0,0 +1,184 @@
|
||||
use fal_client::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{
|
||||
EnqueueFlux2LoraAngleImageSize, EnqueueFlux2LoraAngleNumImages,
|
||||
EnqueueFlux2LoraEditImageAngleRequest,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::request::FalFlux2LoraAnglesRequestState;
|
||||
|
||||
pub fn build_fal_flux_2_lora_angles(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
if image_urls.is_empty() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require at least one input image URL".to_string(),
|
||||
}));
|
||||
}
|
||||
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio, strategy)?;
|
||||
|
||||
let request = EnqueueFlux2LoraEditImageAngleRequest {
|
||||
image_urls,
|
||||
horizontal_angle: builder.horizontal_angle,
|
||||
vertical_angle: builder.vertical_angle,
|
||||
zoom: builder.zoom,
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalFlux2LoraAngles(FalFlux2LoraAnglesRequestState { request }),
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_image_urls(image_inputs: Option<ImageListRef>) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<EnqueueFlux2LoraAngleNumImages, ArtcraftRouterError> {
|
||||
use EnqueueFlux2LoraAngleNumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<EnqueueFlux2LoraAngleImageSize>, ArtcraftRouterError> {
|
||||
use EnqueueFlux2LoraAngleImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::SquareHd)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::Flux2LoraAngles,
|
||||
provider: Provider::Fal,
|
||||
prompt: None,
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/x.jpg".to_string()])),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-15.0),
|
||||
zoom: Some(2.0),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_request(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> EnqueueFlux2LoraEditImageAngleRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalFlux2LoraAngles(state)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected FalFlux2LoraAngles variant")
|
||||
};
|
||||
state.request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_through_camera_params() {
|
||||
let req = unwrap_request(build_fal_flux_2_lora_angles(base()));
|
||||
assert_eq!(req.horizontal_angle, Some(45.0));
|
||||
assert_eq!(req.vertical_angle, Some(-15.0));
|
||||
assert_eq!(req.zoom, Some(2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_image_inputs_errors() {
|
||||
let builder = GenerateImageRequestBuilder { image_inputs: None, ..base() };
|
||||
assert!(build_fal_flux_2_lora_angles(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_rejected() {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new_from_str("mf_test")])),
|
||||
..base()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_2_lora_angles(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_zero_errors() {
|
||||
let builder = GenerateImageRequestBuilder { image_batch_count: Some(0), ..base() };
|
||||
assert!(matches!(
|
||||
build_fal_flux_2_lora_angles(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
}
|
||||
}
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_2_lora_angles::request::FalFlux2LoraAnglesRequestState;
|
||||
|
||||
/// Cost state for Fal Flux 2 LoRA Angles. Delegates to the fal_client cost
|
||||
/// calculator (2¢ per output image — $0.021/MP at the default ~1MP).
|
||||
pub struct FalFlux2LoraAnglesCostState {
|
||||
cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalFlux2LoraAnglesCostState {
|
||||
pub fn from_request(request: &FalFlux2LoraAnglesRequestState) -> Self {
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{
|
||||
EnqueueFlux2LoraAngleNumImages, EnqueueFlux2LoraEditImageAngleRequest,
|
||||
};
|
||||
|
||||
fn cost_for(n: EnqueueFlux2LoraAngleNumImages) -> ImageGenerationCostEstimate {
|
||||
let state = FalFlux2LoraAnglesCostState::from_request(&FalFlux2LoraAnglesRequestState {
|
||||
request: EnqueueFlux2LoraEditImageAngleRequest {
|
||||
image_urls: vec!["https://example.com/x.jpg".to_string()],
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
num_images: Some(n),
|
||||
image_size: None,
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
},
|
||||
});
|
||||
state.estimate_cost()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_is_2_cents() {
|
||||
assert_eq!(cost_for(EnqueueFlux2LoraAngleNumImages::One).cost_in_usd_cents, Some(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_is_4_cents() {
|
||||
assert_eq!(cost_for(EnqueueFlux2LoraAngleNumImages::Two).cost_in_usd_cents, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_images_is_6_cents() {
|
||||
assert_eq!(cost_for(EnqueueFlux2LoraAngleNumImages::Three).cost_in_usd_cents, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_images_is_8_cents() {
|
||||
assert_eq!(cost_for(EnqueueFlux2LoraAngleNumImages::Four).cost_in_usd_cents, Some(8));
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{
|
||||
enqueue_flux_2_lora_edit_image_angle_webhook, EnqueueFlux2LoraEditImageAngleArgs,
|
||||
EnqueueFlux2LoraEditImageAngleRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalFlux2LoraAnglesRequestState {
|
||||
pub request: EnqueueFlux2LoraEditImageAngleRequest,
|
||||
}
|
||||
|
||||
impl FalFlux2LoraAnglesRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
|
||||
let args = EnqueueFlux2LoraEditImageAngleArgs {
|
||||
request: self.request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
let webhook_response = enqueue_flux_2_lora_edit_image_angle_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::webhook::image::angle::enqueue_flux_2_lora_edit_image_angle_webhook::{
|
||||
EnqueueFlux2LoraAngleImageSize, EnqueueFlux2LoraAngleNumImages,
|
||||
};
|
||||
|
||||
fn read_fal_api_key() -> FalApiKey {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
FalApiKey::from_str(secret.trim())
|
||||
}
|
||||
|
||||
fn client_with_webhook() -> RouterFalClient {
|
||||
RouterFalClient::new(
|
||||
read_fal_api_key(),
|
||||
"https://example.com/fal-webhook-test".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_via_webhook() {
|
||||
let client = client_with_webhook();
|
||||
let state = FalFlux2LoraAnglesRequestState {
|
||||
request: EnqueueFlux2LoraEditImageAngleRequest {
|
||||
image_urls: vec!["https://example.com/test.jpg".to_string()],
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-15.0),
|
||||
zoom: Some(2.0),
|
||||
num_images: Some(EnqueueFlux2LoraAngleNumImages::One),
|
||||
image_size: Some(EnqueueFlux2LoraAngleImageSize::SquareHd),
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
},
|
||||
};
|
||||
let response = state.send(&client).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
FluxPro11AspectRatio, FluxPro11NumImages, FluxPro11Request,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
|
||||
|
||||
pub fn build_fal_flux_pro_1p1(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Flux Pro 1.1 is text-to-image only. v1 hard-errors on any image_inputs
|
||||
// (regardless of generation_mode_mismatch_strategy) — match that.
|
||||
if builder.image_inputs.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Flux Pro 1.1 is text-to-image only".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
|
||||
|
||||
let request = FluxPro11Request {
|
||||
prompt: builder.prompt.clone().unwrap_or_default(),
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalFluxPro1p1(FalFluxPro1p1RequestState { request }),
|
||||
))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11AspectRatio, ArtcraftRouterError> {
|
||||
use FluxPro11AspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11NumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FluxPro11NumImages::One),
|
||||
2 => Ok(FluxPro11NumImages::Two),
|
||||
3 => Ok(FluxPro11NumImages::Three),
|
||||
4 => Ok(FluxPro11NumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FluxPro11NumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::FluxPro11,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_request(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> FluxPro11Request {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalFluxPro1p1(state)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected FalFluxPro1p1 variant")
|
||||
};
|
||||
state.request
|
||||
}
|
||||
|
||||
// ── Mode rejection ──
|
||||
|
||||
#[test]
|
||||
fn image_inputs_present_yields_error() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_pro_1p1(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
// ── Prompt ──
|
||||
|
||||
mod prompt_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
prompt: Some("my custom prompt".to_string()),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert_eq!(req.prompt, "my custom prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_defaults_to_empty() {
|
||||
let builder = GenerateImageRequestBuilder { prompt: None, ..base_builder() };
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert_eq!(req.prompt, "");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_square() {
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(base_builder()));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_maps_to_square() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_hd() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::SquareHd));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_sixteen_by_nine() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::LandscapeSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_nine_by_sixteen() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallNineBySixteen),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::PortraitNineBySixteen));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_twenty_one_by_nine_collapses_to_sixteen_by_nine() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::LandscapeSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_four_by_five_collapses_to_three_by_four() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11AspectRatio::PortraitThreeByFour));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Num images ──
|
||||
|
||||
mod num_images_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_one() {
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(base_builder()));
|
||||
assert!(matches!(req.num_images, FluxPro11NumImages::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_three() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(3),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.num_images, FluxPro11NumImages::Three));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_is_error() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_pro_1p1(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_four_error_out() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(7),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_pro_1p1(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_batch_count", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_four_clamps_with_upgrade() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(7),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1(builder));
|
||||
assert!(matches!(req.num_images, FluxPro11NumImages::Four));
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
|
||||
|
||||
pub struct FalFluxPro1p1CostState {
|
||||
cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalFluxPro1p1CostState {
|
||||
pub fn from_request(request: &FalFluxPro1p1RequestState) -> Self {
|
||||
// Delegate to the fal_client cost calculator: $0.04 per image (1MP base).
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
// Matches v1: credits == cents (1:1 with USD cents).
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
FluxPro11AspectRatio, FluxPro11NumImages, FluxPro11Request,
|
||||
};
|
||||
|
||||
fn cost_for(n: FluxPro11NumImages) -> ImageGenerationCostEstimate {
|
||||
let state = FalFluxPro1p1CostState::from_request(&FalFluxPro1p1RequestState {
|
||||
request: FluxPro11Request {
|
||||
prompt: "test".to_string(),
|
||||
aspect_ratio: FluxPro11AspectRatio::Square,
|
||||
num_images: n,
|
||||
},
|
||||
});
|
||||
state.estimate_cost()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_is_4_cents() {
|
||||
assert_eq!(cost_for(FluxPro11NumImages::One).cost_in_usd_cents, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_is_8_cents() {
|
||||
assert_eq!(cost_for(FluxPro11NumImages::Two).cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_images_is_12_cents() {
|
||||
assert_eq!(cost_for(FluxPro11NumImages::Three).cost_in_usd_cents, Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_images_is_16_cents() {
|
||||
assert_eq!(cost_for(FluxPro11NumImages::Four).cost_in_usd_cents, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_equal_cents() {
|
||||
// v1 set cost_in_credits to the same USD cents value; preserve that.
|
||||
let cost = cost_for(FluxPro11NumImages::Two);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_flags_match_v1() {
|
||||
let cost = cost_for(FluxPro11NumImages::One);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.failures_are_refunded.is_none());
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
enqueue_flux_pro_11_text_to_image_webhook, FluxPro11Args, FluxPro11Request,
|
||||
};
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalFluxPro1p1RequestState {
|
||||
pub request: FluxPro11Request,
|
||||
}
|
||||
|
||||
impl FalFluxPro1p1RequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
|
||||
let args = FluxPro11Args {
|
||||
request: self.request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
let webhook_response = enqueue_flux_pro_11_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
|
||||
FluxPro11AspectRatio, FluxPro11NumImages,
|
||||
};
|
||||
|
||||
fn read_fal_api_key() -> FalApiKey {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
FalApiKey::from_str(secret.trim())
|
||||
}
|
||||
|
||||
fn client_with_webhook() -> RouterFalClient {
|
||||
RouterFalClient::new(
|
||||
read_fal_api_key(),
|
||||
"https://example.com/fal-webhook-test".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn t2i_state() -> FalFluxPro1p1RequestState {
|
||||
FalFluxPro1p1RequestState {
|
||||
request: FluxPro11Request {
|
||||
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
|
||||
aspect_ratio: FluxPro11AspectRatio::LandscapeSixteenByNine,
|
||||
num_images: FluxPro11NumImages::One,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_via_webhook() {
|
||||
let client = client_with_webhook();
|
||||
let response = t2i_state().send(&client).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
}
|
||||
+283
@@ -0,0 +1,283 @@
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages, FluxPro11UltraRequest,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
|
||||
|
||||
pub fn build_fal_flux_pro_1p1_ultra(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Flux Pro 1.1 Ultra is text-to-image only. v1 hard-errors on any
|
||||
// image_inputs (regardless of generation_mode_mismatch_strategy) —
|
||||
// match that.
|
||||
if builder.image_inputs.is_some() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Flux Pro 1.1 Ultra is text-to-image only".to_string(),
|
||||
}));
|
||||
}
|
||||
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio, strategy)?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
|
||||
|
||||
let request = FluxPro11UltraRequest {
|
||||
prompt: builder.prompt.clone().unwrap_or_default(),
|
||||
aspect_ratio,
|
||||
num_images,
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalFluxPro1p1Ultra(FalFluxPro1p1UltraRequestState { request }),
|
||||
))
|
||||
}
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11UltraAspectRatio, ArtcraftRouterError> {
|
||||
use FluxPro11UltraAspectRatio as Ar;
|
||||
match aspect_ratio {
|
||||
None => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeThreeByTwo),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeTwentyOneByNine),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitTwoByThree),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
|
||||
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineByTwentyOne),
|
||||
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Ar::Square),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<FluxPro11UltraNumImages, ArtcraftRouterError> {
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(FluxPro11UltraNumImages::One),
|
||||
2 => Ok(FluxPro11UltraNumImages::Two),
|
||||
3 => Ok(FluxPro11UltraNumImages::Three),
|
||||
4 => Ok(FluxPro11UltraNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(FluxPro11UltraNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::FluxPro11Ultra,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_request(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> FluxPro11UltraRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalFluxPro1p1Ultra(state)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected FalFluxPro1p1Ultra variant")
|
||||
};
|
||||
state.request
|
||||
}
|
||||
|
||||
// ── Mode rejection ──
|
||||
|
||||
#[test]
|
||||
fn image_inputs_present_yields_error() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_pro_1p1_ultra(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
// ── Prompt ──
|
||||
|
||||
mod prompt_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
prompt: Some("my custom prompt".to_string()),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert_eq!(req.prompt, "my custom prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_prompt_defaults_to_empty() {
|
||||
let builder = GenerateImageRequestBuilder { prompt: None, ..base_builder() };
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert_eq!(req.prompt, "");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_square() {
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(base_builder()));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_sixteen_by_nine() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::LandscapeSixteenByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_three_by_two_has_direct_mapping() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideThreeByTwo),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::LandscapeThreeByTwo));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tall_two_by_three_has_direct_mapping() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::TallTwoByThree),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::PortraitTwoByThree));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wide_twenty_one_by_nine_has_direct_mapping() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::LandscapeTwentyOneByNine));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn square_hd_collapses_to_square() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::SquareHd),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert!(matches!(req.aspect_ratio, FluxPro11UltraAspectRatio::Square));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Num images ──
|
||||
|
||||
mod num_images_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_one() {
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(base_builder()));
|
||||
assert!(matches!(req.num_images, FluxPro11UltraNumImages::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_is_error() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_pro_1p1_ultra(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_four_error_out() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(5),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_flux_pro_1p1_ultra(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_batch_count", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_four_clamps_with_upgrade() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(7),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_request(build_fal_flux_pro_1p1_ultra(builder));
|
||||
assert!(matches!(req.num_images, FluxPro11UltraNumImages::Four));
|
||||
}
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
|
||||
|
||||
pub struct FalFluxPro1p1UltraCostState {
|
||||
cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalFluxPro1p1UltraCostState {
|
||||
pub fn from_request(request: &FalFluxPro1p1UltraRequestState) -> Self {
|
||||
// Delegate to the fal_client cost calculator: $0.06 per image.
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
// Matches v1: credits == cents (1:1 with USD cents).
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages, FluxPro11UltraRequest,
|
||||
};
|
||||
|
||||
fn cost_for(n: FluxPro11UltraNumImages) -> ImageGenerationCostEstimate {
|
||||
let state = FalFluxPro1p1UltraCostState::from_request(&FalFluxPro1p1UltraRequestState {
|
||||
request: FluxPro11UltraRequest {
|
||||
prompt: "test".to_string(),
|
||||
aspect_ratio: FluxPro11UltraAspectRatio::Square,
|
||||
num_images: n,
|
||||
},
|
||||
});
|
||||
state.estimate_cost()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_is_6_cents() {
|
||||
assert_eq!(cost_for(FluxPro11UltraNumImages::One).cost_in_usd_cents, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_is_12_cents() {
|
||||
assert_eq!(cost_for(FluxPro11UltraNumImages::Two).cost_in_usd_cents, Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_images_is_18_cents() {
|
||||
assert_eq!(cost_for(FluxPro11UltraNumImages::Three).cost_in_usd_cents, Some(18));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_images_is_24_cents() {
|
||||
assert_eq!(cost_for(FluxPro11UltraNumImages::Four).cost_in_usd_cents, Some(24));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_equal_cents() {
|
||||
let cost = cost_for(FluxPro11UltraNumImages::Two);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_flags_match_v1() {
|
||||
let cost = cost_for(FluxPro11UltraNumImages::One);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.failures_are_refunded.is_none());
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
enqueue_flux_pro_11_ultra_text_to_image_webhook, FluxPro11UltraArgs, FluxPro11UltraRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalFluxPro1p1UltraRequestState {
|
||||
pub request: FluxPro11UltraRequest,
|
||||
}
|
||||
|
||||
impl FalFluxPro1p1UltraRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(self.request.clone());
|
||||
let args = FluxPro11UltraArgs {
|
||||
request: self.request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
let webhook_response = enqueue_flux_pro_11_ultra_text_to_image_webhook(args)
|
||||
.await
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
|
||||
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages,
|
||||
};
|
||||
|
||||
fn read_fal_api_key() -> FalApiKey {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
FalApiKey::from_str(secret.trim())
|
||||
}
|
||||
|
||||
fn client_with_webhook() -> RouterFalClient {
|
||||
RouterFalClient::new(
|
||||
read_fal_api_key(),
|
||||
"https://example.com/fal-webhook-test".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn t2i_state() -> FalFluxPro1p1UltraRequestState {
|
||||
FalFluxPro1p1UltraRequestState {
|
||||
request: FluxPro11UltraRequest {
|
||||
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
|
||||
aspect_ratio: FluxPro11UltraAspectRatio::LandscapeSixteenByNine,
|
||||
num_images: FluxPro11UltraNumImages::One,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_via_webhook() {
|
||||
let client = client_with_webhook();
|
||||
let response = t2i_state().send(&client).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("Webhook t2i — request_id: {:?}, gateway_request_id: {:?}", payload.request_id, payload.gateway_request_id);
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
}
|
||||
+116
-21
@@ -1,8 +1,27 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1_edit_image::api::{
|
||||
GptImage1EditImageNumImages, GptImage1EditImageQuality, GptImage1EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1_text_to_image::api::{
|
||||
GptImage1TextToImageNumImages, GptImage1TextToImageQuality, GptImage1TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
|
||||
|
||||
/// Cost state for Fal GPT Image 1. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_gpt_image_1`):
|
||||
///
|
||||
/// Output image cost (per output image):
|
||||
/// Low: 2¢ all sizes
|
||||
/// Medium: 5¢ square, 7¢ wide/tall
|
||||
/// High: 17¢ square, 25¢ wide/tall
|
||||
///
|
||||
/// Plus 2¢ per input image in edit mode (high-fidelity estimate).
|
||||
/// Quality defaults to High when unspecified.
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait
|
||||
/// (which uses Medium as the default and omits the input-image fee). Keep
|
||||
/// in sync with the v1 cost file.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1CostState {
|
||||
request: FalGptImage1RequestState,
|
||||
@@ -15,12 +34,19 @@ impl FalGptImage1CostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1RequestState::TextToImage(req) => {
|
||||
cost_t2i(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
FalGptImage1RequestState::EditImage(req) => {
|
||||
let output_cost = cost_edit_output(req.quality, req.image_size, req.num_images);
|
||||
let input_cost = 2 * req.image_urls.len() as u64;
|
||||
output_cost + input_cost
|
||||
}
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -31,6 +57,61 @@ impl FalGptImage1CostState {
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_t2i(
|
||||
quality: Option<GptImage1TextToImageQuality>,
|
||||
image_size: Option<GptImage1TextToImageSize>,
|
||||
num_images: GptImage1TextToImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1TextToImageQuality as Q;
|
||||
use GptImage1TextToImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let is_square = matches!(image_size, None | Some(S::Square));
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), is_square);
|
||||
per_image * t2i_num_images(num_images)
|
||||
}
|
||||
|
||||
fn cost_edit_output(
|
||||
quality: Option<GptImage1EditImageQuality>,
|
||||
image_size: Option<GptImage1EditImageSize>,
|
||||
num_images: GptImage1EditImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1EditImageQuality as Q;
|
||||
use GptImage1EditImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let is_square = matches!(image_size, None | Some(S::Square));
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), is_square);
|
||||
per_image * edit_num_images(num_images)
|
||||
}
|
||||
|
||||
fn output_cost(is_low: bool, is_medium: bool, is_square: bool) -> u64 {
|
||||
if is_low {
|
||||
2
|
||||
} else if is_medium {
|
||||
if is_square { 5 } else { 7 }
|
||||
} else {
|
||||
// High (default)
|
||||
if is_square { 17 } else { 25 }
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: GptImage1TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1TextToImageNumImages::One => 1,
|
||||
GptImage1TextToImageNumImages::Two => 2,
|
||||
GptImage1TextToImageNumImages::Three => 3,
|
||||
GptImage1TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: GptImage1EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1EditImageNumImages::One => 1,
|
||||
GptImage1EditImageNumImages::Two => 2,
|
||||
GptImage1EditImageNumImages::Three => 3,
|
||||
GptImage1EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -60,39 +141,53 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// Tables encode v1's pricing semantics:
|
||||
// - Quality defaults to High when unspecified.
|
||||
// - is_square = matches!(image_size, None | Some(Square)). Auto, Horizontal,
|
||||
// and Vertical are treated as non-square.
|
||||
// - Low is 2¢ for every size; Medium and High differ by square vs non-square.
|
||||
// Edit-mode rows additionally include a +2¢ input-image fee (one input URL
|
||||
// in the make_edit helper below).
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1TextToImageQuality>, Option<GptImage1TextToImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
// Default quality (None → High) + default size (None → square).
|
||||
(None, None, [17, 34, 51, 68]),
|
||||
// Low: flat 2¢ regardless of size.
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Low), Some(GptImage1TextToImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto), [5, 10, 15, 20]),
|
||||
// Medium: 5¢ square, 7¢ non-square.
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Auto), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::Medium), Some(GptImage1TextToImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Auto), [17, 34, 51, 68]),
|
||||
// High: 17¢ square, 25¢ non-square.
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Auto), [25, 50, 75, 100]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1TextToImageQuality::High), Some(GptImage1TextToImageSize::Vertical), [25, 50, 75, 100]),
|
||||
]
|
||||
}
|
||||
|
||||
// Each edit-mode row is t2i row + 2¢ (single input image — see make_edit
|
||||
// below). Numbers are constant +2 because input fee scales with input count,
|
||||
// not output count.
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1EditImageQuality>, Option<GptImage1EditImageSize>, [u64; 4]); 13] {
|
||||
[
|
||||
(None, None, [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Vertical), [2, 4, 6, 8]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square), [5, 10, 15, 20]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical), [7, 14, 21, 28]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Auto), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square), [17, 34, 51, 68]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal), [25, 50, 75, 100]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical), [25, 50, 75, 100]),
|
||||
(None, None, [19, 36, 53, 70]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Auto), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Square), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Horizontal), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Low), Some(GptImage1EditImageSize::Vertical), [4, 6, 8, 10]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Auto), [9, 16, 23, 30]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Square), [7, 12, 17, 22]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Horizontal), [9, 16, 23, 30]),
|
||||
(Some(GptImage1EditImageQuality::Medium), Some(GptImage1EditImageSize::Vertical), [9, 16, 23, 30]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Auto), [27, 52, 77, 102]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Square), [19, 36, 53, 70]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Horizontal), [27, 52, 77, 102]),
|
||||
(Some(GptImage1EditImageQuality::High), Some(GptImage1EditImageSize::Vertical), [27, 52, 77, 102]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -106,7 +201,7 @@ mod tests {
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
|
||||
+126
-7
@@ -1,8 +1,26 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::gpt_image_1p5_edit_image::api::{
|
||||
GptImage1p5EditImageNumImages, GptImage1p5EditImageQuality, GptImage1p5EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_1p5_text_to_image::api::{
|
||||
GptImage1p5TextToImageNumImages, GptImage1p5TextToImageQuality, GptImage1p5TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
|
||||
|
||||
/// Cost state for Fal GPT Image 1.5. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_gpt_image_1p5`):
|
||||
///
|
||||
/// Output image cost (per output image), rounded up to whole cents:
|
||||
/// Low: 1¢ square, 2¢ wide/tall
|
||||
/// Medium: 4¢ square, 5¢ wide, 6¢ tall
|
||||
/// High: 14¢ square, 20¢ wide/tall
|
||||
///
|
||||
/// Quality defaults to High when unspecified.
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait,
|
||||
/// which rounds $0.133→13¢ for High+Square (v1 rounds up to 14¢) and
|
||||
/// flattens Medium wide/tall to a single value. Keep in sync with v1.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage1p5CostState {
|
||||
request: FalGptImage1p5RequestState,
|
||||
@@ -15,12 +33,17 @@ impl FalGptImage1p5CostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage1p5RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1p5RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage1p5RequestState::TextToImage(req) => {
|
||||
cost_t2i(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
FalGptImage1p5RequestState::EditImage(req) => {
|
||||
cost_edit(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -31,6 +54,76 @@ impl FalGptImage1p5CostState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum SizeBucket { Square, Wide, Tall }
|
||||
|
||||
fn cost_t2i(
|
||||
quality: Option<GptImage1p5TextToImageQuality>,
|
||||
image_size: Option<GptImage1p5TextToImageSize>,
|
||||
num_images: GptImage1p5TextToImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1p5TextToImageQuality as Q;
|
||||
use GptImage1p5TextToImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let bucket = match image_size {
|
||||
None | Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::Wide) => SizeBucket::Wide,
|
||||
Some(S::Tall) => SizeBucket::Tall,
|
||||
};
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket);
|
||||
per_image * t2i_num_images(num_images)
|
||||
}
|
||||
|
||||
fn cost_edit(
|
||||
quality: Option<GptImage1p5EditImageQuality>,
|
||||
image_size: Option<GptImage1p5EditImageSize>,
|
||||
num_images: GptImage1p5EditImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage1p5EditImageQuality as Q;
|
||||
use GptImage1p5EditImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let bucket = match image_size {
|
||||
None | Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::Wide) => SizeBucket::Wide,
|
||||
Some(S::Tall) => SizeBucket::Tall,
|
||||
};
|
||||
let per_image = output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket);
|
||||
per_image * edit_num_images(num_images)
|
||||
}
|
||||
|
||||
fn output_cost(is_low: bool, is_medium: bool, bucket: SizeBucket) -> u64 {
|
||||
if is_low {
|
||||
match bucket { SizeBucket::Square => 1, _ => 2 }
|
||||
} else if is_medium {
|
||||
match bucket {
|
||||
SizeBucket::Square => 4,
|
||||
SizeBucket::Wide => 5,
|
||||
SizeBucket::Tall => 6,
|
||||
}
|
||||
} else {
|
||||
// High (default)
|
||||
match bucket { SizeBucket::Square => 14, _ => 20 }
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: GptImage1p5TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1p5TextToImageNumImages::One => 1,
|
||||
GptImage1p5TextToImageNumImages::Two => 2,
|
||||
GptImage1p5TextToImageNumImages::Three => 3,
|
||||
GptImage1p5TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: GptImage1p5EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage1p5EditImageNumImages::One => 1,
|
||||
GptImage1p5EditImageNumImages::Two => 2,
|
||||
GptImage1p5EditImageNumImages::Three => 3,
|
||||
GptImage1p5EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -60,12 +153,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// v1 rates: Low=1/2 (sq/wide-tall), Medium=4/5/6 (sq/wide/tall),
|
||||
// High=14/20/20 (sq/wide/tall). Default quality is High.
|
||||
fn text_to_image_price_cases() -> [(Option<GptImage1p5TextToImageQuality>, Option<GptImage1p5TextToImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall), [20, 40, 60, 80])]
|
||||
[
|
||||
(None, None, [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Wide), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5TextToImageQuality::Low), Some(GptImage1p5TextToImageSize::Tall), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Square), [4, 8, 12, 16]),
|
||||
(Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Wide), [5, 10, 15, 20]),
|
||||
(Some(GptImage1p5TextToImageQuality::Medium), Some(GptImage1p5TextToImageSize::Tall), [6, 12, 18, 24]),
|
||||
(Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Square), [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Wide), [20, 40, 60, 80]),
|
||||
(Some(GptImage1p5TextToImageQuality::High), Some(GptImage1p5TextToImageSize::Tall), [20, 40, 60, 80]),
|
||||
]
|
||||
}
|
||||
|
||||
// Edit-mode pricing is identical to t2i (v1 has no input-image fee for
|
||||
// gpt_image_1p5 — that's only gpt_image_1).
|
||||
fn edit_image_price_cases() -> [(Option<GptImage1p5EditImageQuality>, Option<GptImage1p5EditImageSize>, [u64; 4]); 10] {
|
||||
[(None, None, [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall), [1, 2, 3, 4]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square), [3, 6, 9, 12]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall), [5, 10, 15, 20]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square), [13, 26, 39, 52]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide), [20, 40, 60, 80]), (Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall), [20, 40, 60, 80])]
|
||||
[
|
||||
(None, None, [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Wide), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5EditImageQuality::Low), Some(GptImage1p5EditImageSize::Tall), [2, 4, 6, 8]),
|
||||
(Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Square), [4, 8, 12, 16]),
|
||||
(Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Wide), [5, 10, 15, 20]),
|
||||
(Some(GptImage1p5EditImageQuality::Medium), Some(GptImage1p5EditImageSize::Tall), [6, 12, 18, 24]),
|
||||
(Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Square), [14, 28, 42, 56]),
|
||||
(Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Wide), [20, 40, 60, 80]),
|
||||
(Some(GptImage1p5EditImageQuality::High), Some(GptImage1p5EditImageSize::Tall), [20, 40, 60, 80]),
|
||||
]
|
||||
}
|
||||
|
||||
fn text_to_image_num_image_cases(expected_by_count: [u64; 4]) -> [(GptImage1p5TextToImageNumImages, u64); 4] {
|
||||
@@ -78,7 +197,7 @@ mod tests {
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
|
||||
+190
-138
@@ -1,8 +1,28 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{
|
||||
GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageSize,
|
||||
};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{
|
||||
GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageSize,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
|
||||
|
||||
/// Cost state for Fal GPT Image 2. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_gpt_image_2`):
|
||||
///
|
||||
/// Output image cost (per output image), rounded up to whole cents:
|
||||
/// Low: 1¢ all sizes
|
||||
/// Medium: 6¢ square/squareHD/auto; 4¢ landscape/portrait (4:3 or 16:9)
|
||||
/// High: 22¢ square; 23¢ squareHD/auto;
|
||||
/// 15¢ landscape/portrait 4:3; 16¢ landscape/portrait 16:9
|
||||
///
|
||||
/// Quality defaults to High when unspecified. Resolution is NOT priced
|
||||
/// separately — v1's table only varies by quality + size.
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait,
|
||||
/// which uses a base+per-megapixel formula that scales with the resolution
|
||||
/// field. Keep in sync with v1.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FalGptImage2CostState {
|
||||
request: FalGptImage2RequestState,
|
||||
@@ -15,12 +35,17 @@ impl FalGptImage2CostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
let cost_in_usd_cents = match &self.request {
|
||||
FalGptImage2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
FalGptImage2RequestState::TextToImage(req) => {
|
||||
cost_t2i(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
FalGptImage2RequestState::EditImage(req) => {
|
||||
cost_edit(req.quality, req.image_size, req.num_images)
|
||||
}
|
||||
};
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -31,22 +56,115 @@ impl FalGptImage2CostState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
enum SizeBucket {
|
||||
Square,
|
||||
SquareHd,
|
||||
Landscape4x3,
|
||||
Landscape16x9,
|
||||
Portrait4x3,
|
||||
Portrait16x9,
|
||||
Auto,
|
||||
}
|
||||
|
||||
fn cost_t2i(
|
||||
quality: Option<GptImage2TextToImageQuality>,
|
||||
image_size: Option<GptImage2TextToImageSize>,
|
||||
num_images: GptImage2TextToImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage2TextToImageQuality as Q;
|
||||
use GptImage2TextToImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
// T2I size enum has no Auto variant — `None` is the only auto-equivalent.
|
||||
let bucket = match image_size {
|
||||
None => SizeBucket::Auto,
|
||||
Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::SquareHd) => SizeBucket::SquareHd,
|
||||
Some(S::Landscape4x3) => SizeBucket::Landscape4x3,
|
||||
Some(S::Landscape16x9) => SizeBucket::Landscape16x9,
|
||||
Some(S::Portrait4x3) => SizeBucket::Portrait4x3,
|
||||
Some(S::Portrait16x9) => SizeBucket::Portrait16x9,
|
||||
};
|
||||
output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket) * t2i_num_images(num_images)
|
||||
}
|
||||
|
||||
fn cost_edit(
|
||||
quality: Option<GptImage2EditImageQuality>,
|
||||
image_size: Option<GptImage2EditImageSize>,
|
||||
num_images: GptImage2EditImageNumImages,
|
||||
) -> u64 {
|
||||
use GptImage2EditImageQuality as Q;
|
||||
use GptImage2EditImageSize as S;
|
||||
let q = quality.unwrap_or(Q::High);
|
||||
let bucket = match image_size {
|
||||
None | Some(S::Auto) => SizeBucket::Auto,
|
||||
Some(S::Square) => SizeBucket::Square,
|
||||
Some(S::SquareHd) => SizeBucket::SquareHd,
|
||||
Some(S::Landscape4x3) => SizeBucket::Landscape4x3,
|
||||
Some(S::Landscape16x9) => SizeBucket::Landscape16x9,
|
||||
Some(S::Portrait4x3) => SizeBucket::Portrait4x3,
|
||||
Some(S::Portrait16x9) => SizeBucket::Portrait16x9,
|
||||
};
|
||||
output_cost(matches!(q, Q::Low), matches!(q, Q::Medium), bucket) * edit_num_images(num_images)
|
||||
}
|
||||
|
||||
fn output_cost(is_low: bool, is_medium: bool, bucket: SizeBucket) -> u64 {
|
||||
if is_low {
|
||||
return 1;
|
||||
}
|
||||
if is_medium {
|
||||
return match bucket {
|
||||
SizeBucket::Square | SizeBucket::SquareHd | SizeBucket::Auto => 6,
|
||||
SizeBucket::Landscape4x3 | SizeBucket::Portrait4x3
|
||||
| SizeBucket::Landscape16x9 | SizeBucket::Portrait16x9 => 4,
|
||||
};
|
||||
}
|
||||
// High (default)
|
||||
match bucket {
|
||||
SizeBucket::Square => 22,
|
||||
SizeBucket::SquareHd | SizeBucket::Auto => 23,
|
||||
SizeBucket::Landscape4x3 | SizeBucket::Portrait4x3 => 15,
|
||||
SizeBucket::Landscape16x9 | SizeBucket::Portrait16x9 => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: GptImage2TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage2TextToImageNumImages::One => 1,
|
||||
GptImage2TextToImageNumImages::Two => 2,
|
||||
GptImage2TextToImageNumImages::Three => 3,
|
||||
GptImage2TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: GptImage2EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
GptImage2EditImageNumImages::One => 1,
|
||||
GptImage2EditImageNumImages::Two => 2,
|
||||
GptImage2EditImageNumImages::Three => 3,
|
||||
GptImage2EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use fal_client::requests::api::image::common::gpt_image_2_resolution::GptImage2Resolution;
|
||||
use fal_client::requests::api::image::edit::gpt_image_2_edit_image::api::{GptImage2EditImageNumImages, GptImage2EditImageQuality, GptImage2EditImageRequest, GptImage2EditImageSize};
|
||||
use fal_client::requests::api::image::text::gpt_image_2_text_to_image::api::{GptImage2TextToImageNumImages, GptImage2TextToImageQuality, GptImage2TextToImageRequest, GptImage2TextToImageSize};
|
||||
|
||||
type TextPriceCase = (Option<GptImage2TextToImageQuality>, Option<GptImage2TextToImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
type EditPriceCase = (Option<GptImage2EditImageQuality>, Option<GptImage2EditImageSize>, Option<GptImage2Resolution>, [u64; 4]);
|
||||
type TextPriceCase = (Option<GptImage2TextToImageQuality>, Option<GptImage2TextToImageSize>, [u64; 4]);
|
||||
type EditPriceCase = (Option<GptImage2EditImageQuality>, Option<GptImage2EditImageSize>, [u64; 4]);
|
||||
|
||||
// v1 doesn't price by resolution — only quality + size. The Fal request's
|
||||
// resolution field is sent to Fal for actual generation but doesn't enter
|
||||
// the cost calculation.
|
||||
|
||||
#[test]
|
||||
fn text_to_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in text_to_image_price_cases() {
|
||||
fn text_to_image_cost_table_covers_quality_and_size() {
|
||||
for (quality, image_size, expected_by_count) in text_to_image_price_cases() {
|
||||
for (num_images, expected) in text_to_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, resolution, quality, output_format: None });
|
||||
let request = FalGptImage2RequestState::TextToImage(GptImage2TextToImageRequest { prompt: "test".to_string(), num_images, image_size, resolution: None, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
@@ -54,146 +172,80 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_image_cost_table_covers_quality_size_resolution_and_count() {
|
||||
for (quality, image_size, resolution, expected_by_count) in edit_image_price_cases() {
|
||||
fn edit_image_cost_table_covers_quality_and_size() {
|
||||
for (quality, image_size, expected_by_count) in edit_image_price_cases() {
|
||||
for (num_images, expected) in edit_image_num_image_cases(expected_by_count) {
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, resolution, quality, output_format: None });
|
||||
let request = FalGptImage2RequestState::EditImage(GptImage2EditImageRequest { prompt: "test".to_string(), image_urls: vec!["https://example.com/image.png".to_string()], num_images, mask_url: None, image_size, resolution: None, quality, output_format: None });
|
||||
let cost = FalGptImage2CostState::from_request(&request).estimate_cost();
|
||||
assert_standard_cost_estimate(cost, expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// v1 rates (independent of resolution):
|
||||
// Low: 1¢ all sizes
|
||||
// Medium: 6¢ Square/SquareHd/Auto; 4¢ Landscape/Portrait (4:3 or 16:9)
|
||||
// High: 22¢ Square; 23¢ SquareHd/Auto; 15¢ 4:3 (L/P); 16¢ 16:9 (L/P)
|
||||
// Default quality is High.
|
||||
fn text_to_image_price_cases() -> Vec<TextPriceCase> {
|
||||
let mut cases = vec![
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [16, 32, 48, 64]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), None, [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), None, [8, 16, 24, 32]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), None, [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), None, [20, 40, 60, 80]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), None, [27, 54, 81, 108]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in text_to_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2TextToImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2TextToImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2TextToImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
let mut cases = vec![
|
||||
// Default: High quality, Square
|
||||
(None, None, None, [15, 30, 45, 60]),
|
||||
// Auto (with and without resolution — same cost, conservative max)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), None, [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), None, [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), None, [41, 82, 123, 164]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [2, 4, 6, 8]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [10, 20, 30, 40]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), Some(GptImage2Resolution::OneK), [41, 82, 123, 164]),
|
||||
// Presets (no resolution)
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), None, [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), None, [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), None, [5, 10, 15, 20]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), None, [7, 14, 21, 28]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), None, [14, 28, 42, 56]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), None, [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), None, [19, 38, 57, 76]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), None, [26, 52, 78, 104]),
|
||||
];
|
||||
|
||||
for (size, resolution, low, medium, high) in edit_image_resolution_price_cases() {
|
||||
cases.push((Some(GptImage2EditImageQuality::Low), Some(size), Some(resolution), low));
|
||||
cases.push((Some(GptImage2EditImageQuality::Medium), Some(size), Some(resolution), medium));
|
||||
cases.push((Some(GptImage2EditImageQuality::High), Some(size), Some(resolution), high));
|
||||
}
|
||||
|
||||
cases
|
||||
}
|
||||
|
||||
fn text_to_image_resolution_price_cases() -> Vec<(GptImage2TextToImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
vec![
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Landscape4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Landscape16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::TwoK, [2, 4, 6, 8], [7, 14, 21, 28], [24, 48, 72, 96]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [11, 22, 33, 44], [37, 74, 111, 148]),
|
||||
(GptImage2TextToImageSize::Portrait4x3, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [41, 82, 123, 164]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [15, 30, 45, 60]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::TwoK, [2, 4, 6, 8], [6, 12, 18, 24], [21, 42, 63, 84]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [31, 62, 93, 124]),
|
||||
(GptImage2TextToImageSize::Portrait16x9, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::Square, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [5, 10, 15, 20], [16, 32, 48, 64]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::TwoK, [2, 4, 6, 8], [8, 16, 24, 32], [27, 54, 81, 108]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::ThreeK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(GptImage2TextToImageSize::SquareHd, GptImage2Resolution::FourK, [3, 6, 9, 12], [12, 24, 36, 48], [42, 84, 126, 168]),
|
||||
(None, None, [23, 46, 69, 92]),
|
||||
// Low
|
||||
// No Auto variant on t2i — None is the only auto-equivalent (tested above).
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::SquareHd), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Landscape16x9), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2TextToImageQuality::Low), Some(GptImage2TextToImageSize::Portrait16x9), [1, 2, 3, 4]),
|
||||
// Medium
|
||||
// No Auto variant on t2i.
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Square), [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::SquareHd), [6, 12, 18, 24]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Landscape16x9), [4, 8, 12, 16]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2TextToImageQuality::Medium), Some(GptImage2TextToImageSize::Portrait16x9), [4, 8, 12, 16]),
|
||||
// High
|
||||
// No Auto variant on t2i.
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Square), [22, 44, 66, 88]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::SquareHd), [23, 46, 69, 92]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Landscape16x9), [16, 32, 48, 64]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2TextToImageQuality::High), Some(GptImage2TextToImageSize::Portrait16x9), [16, 32, 48, 64]),
|
||||
]
|
||||
}
|
||||
|
||||
fn edit_image_resolution_price_cases() -> Vec<(GptImage2EditImageSize, GptImage2Resolution, [u64; 4], [u64; 4], [u64; 4])> {
|
||||
// Edit-mode pricing is identical to t2i (v1 has no input-image fee for
|
||||
// gpt_image_2 — only gpt_image_1 has that fee).
|
||||
fn edit_image_price_cases() -> Vec<EditPriceCase> {
|
||||
vec![
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Landscape4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Landscape16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::TwoK, [1, 2, 3, 4], [6, 12, 18, 24], [23, 46, 69, 92]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [9, 18, 27, 36], [36, 72, 108, 144]),
|
||||
(GptImage2EditImageSize::Portrait4x3, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [40, 80, 120, 160]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [14, 28, 42, 56]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::TwoK, [1, 2, 3, 4], [5, 10, 15, 20], [20, 40, 60, 80]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::ThreeK, [1, 2, 3, 4], [8, 16, 24, 32], [30, 60, 90, 120]),
|
||||
(GptImage2EditImageSize::Portrait16x9, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::Square, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::OneK, [1, 2, 3, 4], [4, 8, 12, 16], [15, 30, 45, 60]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::TwoK, [1, 2, 3, 4], [7, 14, 21, 28], [26, 52, 78, 104]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::ThreeK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(GptImage2EditImageSize::SquareHd, GptImage2Resolution::FourK, [2, 4, 6, 8], [10, 20, 30, 40], [41, 82, 123, 164]),
|
||||
(None, None, [23, 46, 69, 92]),
|
||||
// Low
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Auto), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Square), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::SquareHd), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Landscape16x9), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait4x3), [1, 2, 3, 4]),
|
||||
(Some(GptImage2EditImageQuality::Low), Some(GptImage2EditImageSize::Portrait16x9), [1, 2, 3, 4]),
|
||||
// Medium
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Auto), [6, 12, 18, 24]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Square), [6, 12, 18, 24]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::SquareHd), [6, 12, 18, 24]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Landscape16x9), [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait4x3), [4, 8, 12, 16]),
|
||||
(Some(GptImage2EditImageQuality::Medium), Some(GptImage2EditImageSize::Portrait16x9), [4, 8, 12, 16]),
|
||||
// High
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Auto), [23, 46, 69, 92]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Square), [22, 44, 66, 88]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::SquareHd), [23, 46, 69, 92]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Landscape16x9), [16, 32, 48, 64]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait4x3), [15, 30, 45, 60]),
|
||||
(Some(GptImage2EditImageQuality::High), Some(GptImage2EditImageSize::Portrait16x9), [16, 32, 48, 64]),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -207,7 +259,7 @@ mod tests {
|
||||
|
||||
fn assert_standard_cost_estimate(cost: ImageGenerationCostEstimate, expected_usd_cents: u64) {
|
||||
assert_eq!(cost.cost_in_usd_cents, Some(expected_usd_cents));
|
||||
assert_eq!(cost.cost_in_credits, None);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
assert!(!cost.is_free);
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
|
||||
+11
@@ -1,7 +1,18 @@
|
||||
pub mod flux_1_dev;
|
||||
pub mod flux_1_schnell;
|
||||
pub mod flux_2_lora_angles;
|
||||
pub mod flux_pro_1p1;
|
||||
pub mod flux_pro_1p1_ultra;
|
||||
pub mod gpt_image_1;
|
||||
pub mod gpt_image_1p5;
|
||||
pub mod gpt_image_2;
|
||||
pub mod nano_banana;
|
||||
pub mod nano_banana_2;
|
||||
pub mod nano_banana_pro;
|
||||
pub mod qwen_edit_2511_angles;
|
||||
pub mod seedream_4;
|
||||
pub mod seedream_4p5;
|
||||
pub mod seedream_5_lite;
|
||||
|
||||
#[cfg(test)]
|
||||
mod cost_parity_tests;
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{
|
||||
Gemini25FlashEditAspectRatio, Gemini25FlashEditNumImages, Gemini25FlashEditRequest,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{
|
||||
Gemini25FlashTextToImageAspectRatio, Gemini25FlashTextToImageNumImages,
|
||||
Gemini25FlashTextToImageRequest,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana::request::FalNanoBananaRequestState;
|
||||
|
||||
pub fn build_fal_nano_banana(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_list_ref(builder.image_inputs.clone())?;
|
||||
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
|
||||
let prompt = builder.prompt.clone().unwrap_or_default();
|
||||
|
||||
let state = if image_urls.is_empty() {
|
||||
let aspect_ratio = plan_t2i_aspect_ratio(builder.aspect_ratio, strategy)?;
|
||||
FalNanoBananaRequestState::TextToImage(Gemini25FlashTextToImageRequest {
|
||||
prompt,
|
||||
num_images: to_t2i_num_images(num_images),
|
||||
aspect_ratio,
|
||||
})
|
||||
} else {
|
||||
let aspect_ratio = plan_edit_aspect_ratio(builder.aspect_ratio, strategy)?;
|
||||
FalNanoBananaRequestState::EditImage(Gemini25FlashEditRequest {
|
||||
prompt,
|
||||
image_urls,
|
||||
num_images: to_edit_num_images(num_images),
|
||||
aspect_ratio,
|
||||
})
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalNanoBanana(state),
|
||||
))
|
||||
}
|
||||
|
||||
// ── Num images ──
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
enum PlannedNumImages {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<PlannedNumImages, ArtcraftRouterError> {
|
||||
let count = count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(PlannedNumImages::One),
|
||||
2 => Ok(PlannedNumImages::Two),
|
||||
3 => Ok(PlannedNumImages::Three),
|
||||
4 => Ok(PlannedNumImages::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(PlannedNumImages::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn to_t2i_num_images(n: PlannedNumImages) -> Gemini25FlashTextToImageNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => Gemini25FlashTextToImageNumImages::One,
|
||||
PlannedNumImages::Two => Gemini25FlashTextToImageNumImages::Two,
|
||||
PlannedNumImages::Three => Gemini25FlashTextToImageNumImages::Three,
|
||||
PlannedNumImages::Four => Gemini25FlashTextToImageNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_edit_num_images(n: PlannedNumImages) -> Gemini25FlashEditNumImages {
|
||||
match n {
|
||||
PlannedNumImages::One => Gemini25FlashEditNumImages::One,
|
||||
PlannedNumImages::Two => Gemini25FlashEditNumImages::Two,
|
||||
PlannedNumImages::Three => Gemini25FlashEditNumImages::Three,
|
||||
PlannedNumImages::Four => Gemini25FlashEditNumImages::Four,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
// Gemini 2.5 Flash text-to-image supported aspect ratios:
|
||||
// 1:1, 5:4, 4:3, 3:2, 16:9, 21:9, 4:5, 3:4, 2:3, 9:16
|
||||
// (no Auto for text-to-image)
|
||||
fn plan_t2i_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Gemini25FlashTextToImageAspectRatio>, ArtcraftRouterError> {
|
||||
use Gemini25FlashTextToImageAspectRatio as T;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(T::OneByOne)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(T::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(T::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(T::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(T::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(T::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(T::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(T::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(T::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(T::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(T::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(T::NineBySixteen)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Gemini 2.5 Flash image-edit supported aspect ratios: same as t2i plus Auto
|
||||
fn plan_edit_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<Gemini25FlashEditAspectRatio>, ArtcraftRouterError> {
|
||||
use Gemini25FlashEditAspectRatio as E;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(E::Auto)),
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(E::OneByOne)),
|
||||
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(E::FiveByFour)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(E::FourByThree)),
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(E::ThreeByTwo)),
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(E::SixteenByNine)),
|
||||
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(E::TwentyOneByNine)),
|
||||
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(E::FourByFive)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(E::ThreeByFour)),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(E::TwoByThree)),
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(E::NineBySixteen)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(E::NineBySixteen)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image inputs ──
|
||||
|
||||
fn resolve_image_list_ref(
|
||||
image_list_ref: Option<ImageListRef>,
|
||||
) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_list_ref {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base_builder() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::NanoBanana,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("a cat in space".to_string()),
|
||||
image_inputs: None,
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_t2i(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> Gemini25FlashTextToImageRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalNanoBanana(FalNanoBananaRequestState::TextToImage(req))
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected TextToImage variant")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
fn unwrap_edit(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> Gemini25FlashEditRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalNanoBanana(FalNanoBananaRequestState::EditImage(req))
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected EditImage variant")
|
||||
};
|
||||
req
|
||||
}
|
||||
|
||||
// ── Mode detection ──
|
||||
|
||||
mod mode_detection {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn no_image_inputs_yields_text_to_image() {
|
||||
let req = unwrap_t2i(build_fal_nano_banana(base_builder()));
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_inputs_yields_edit() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_edit(build_fal_nano_banana(builder));
|
||||
assert_eq!(req.image_urls, vec!["https://example.com/img.jpg".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_image_inputs_all_passed_through() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/a.jpg".to_string(),
|
||||
"https://example.com/b.jpg".to_string(),
|
||||
])),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_edit(build_fal_nano_banana(builder));
|
||||
assert_eq!(req.image_urls.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_image_urls_yields_text_to_image() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec![])),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana(builder));
|
||||
assert_eq!(req.prompt, "a cat in space");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_tokens_return_error() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![])),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_nano_banana(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn t2i_default_is_none() {
|
||||
let req = unwrap_t2i(build_fal_nano_banana(base_builder()));
|
||||
assert!(req.aspect_ratio.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2i_auto_falls_back_to_one_by_one() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana(builder));
|
||||
assert!(matches!(req.aspect_ratio, Some(Gemini25FlashTextToImageAspectRatio::OneByOne)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2i_wide_sixteen_by_nine() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana(builder));
|
||||
assert!(matches!(req.aspect_ratio, Some(Gemini25FlashTextToImageAspectRatio::SixteenByNine)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_auto_is_preserved() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/img.jpg".to_string()])),
|
||||
aspect_ratio: Some(CommonAspectRatio::Auto),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_edit(build_fal_nano_banana(builder));
|
||||
assert!(matches!(req.aspect_ratio, Some(Gemini25FlashEditAspectRatio::Auto)));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Num images ──
|
||||
|
||||
mod num_images_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn default_is_one() {
|
||||
let req = unwrap_t2i(build_fal_nano_banana(base_builder()));
|
||||
assert!(matches!(req.num_images, Gemini25FlashTextToImageNumImages::One));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_is_three() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(3),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana(builder));
|
||||
assert!(matches!(req.num_images, Gemini25FlashTextToImageNumImages::Three));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_is_error() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(0),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_nano_banana(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_four_error_out() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(9),
|
||||
..base_builder()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_nano_banana(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_batch_count", .. }))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn over_four_clamps_with_upgrade() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(9),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana(builder));
|
||||
assert!(matches!(req.num_images, Gemini25FlashTextToImageNumImages::Four));
|
||||
}
|
||||
}
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::Gemini25FlashEditNumImages;
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::Gemini25FlashTextToImageNumImages;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana::request::FalNanoBananaRequestState;
|
||||
|
||||
pub struct FalNanoBananaCostState {
|
||||
num_images: u64,
|
||||
}
|
||||
|
||||
impl FalNanoBananaCostState {
|
||||
pub fn from_request(request: &FalNanoBananaRequestState) -> Self {
|
||||
let num_images = match request {
|
||||
FalNanoBananaRequestState::TextToImage(req) => match req.num_images {
|
||||
Gemini25FlashTextToImageNumImages::One => 1,
|
||||
Gemini25FlashTextToImageNumImages::Two => 2,
|
||||
Gemini25FlashTextToImageNumImages::Three => 3,
|
||||
Gemini25FlashTextToImageNumImages::Four => 4,
|
||||
},
|
||||
FalNanoBananaRequestState::EditImage(req) => match req.num_images {
|
||||
Gemini25FlashEditNumImages::One => 1,
|
||||
Gemini25FlashEditNumImages::Two => 2,
|
||||
Gemini25FlashEditNumImages::Three => 3,
|
||||
Gemini25FlashEditNumImages::Four => 4,
|
||||
},
|
||||
};
|
||||
Self { num_images }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
// Pricing: $0.039/image — rounded up to 4 cents (no resolution multiplier,
|
||||
// Gemini 2.5 Flash has no resolution option). Matches v1.
|
||||
const COST_PER_IMAGE: u64 = 4;
|
||||
let cost_in_usd_cents = COST_PER_IMAGE * self.num_images;
|
||||
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::Gemini25FlashEditRequest;
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::Gemini25FlashTextToImageRequest;
|
||||
|
||||
fn t2i_cost(n: Gemini25FlashTextToImageNumImages) -> ImageGenerationCostEstimate {
|
||||
FalNanoBananaCostState::from_request(&FalNanoBananaRequestState::TextToImage(
|
||||
Gemini25FlashTextToImageRequest {
|
||||
prompt: "test".to_string(),
|
||||
num_images: n,
|
||||
aspect_ratio: None,
|
||||
},
|
||||
)).estimate_cost()
|
||||
}
|
||||
|
||||
fn edit_cost(n: Gemini25FlashEditNumImages) -> ImageGenerationCostEstimate {
|
||||
FalNanoBananaCostState::from_request(&FalNanoBananaRequestState::EditImage(
|
||||
Gemini25FlashEditRequest {
|
||||
prompt: "test".to_string(),
|
||||
image_urls: vec!["https://example.com/x.jpg".to_string()],
|
||||
num_images: n,
|
||||
aspect_ratio: None,
|
||||
},
|
||||
)).estimate_cost()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2i_one_image_is_4_cents() {
|
||||
assert_eq!(t2i_cost(Gemini25FlashTextToImageNumImages::One).cost_in_usd_cents, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2i_two_images_is_8_cents() {
|
||||
assert_eq!(t2i_cost(Gemini25FlashTextToImageNumImages::Two).cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2i_three_images_is_12_cents() {
|
||||
assert_eq!(t2i_cost(Gemini25FlashTextToImageNumImages::Three).cost_in_usd_cents, Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn t2i_four_images_is_16_cents() {
|
||||
assert_eq!(t2i_cost(Gemini25FlashTextToImageNumImages::Four).cost_in_usd_cents, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn edit_two_images_matches_t2i_two_images() {
|
||||
assert_eq!(
|
||||
edit_cost(Gemini25FlashEditNumImages::Two).cost_in_usd_cents,
|
||||
t2i_cost(Gemini25FlashTextToImageNumImages::Two).cost_in_usd_cents,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_equal_cents() {
|
||||
let c = t2i_cost(Gemini25FlashTextToImageNumImages::Two);
|
||||
assert_eq!(c.cost_in_credits, c.cost_in_usd_cents);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_flags_match_v1() {
|
||||
let c = t2i_cost(Gemini25FlashTextToImageNumImages::One);
|
||||
assert!(!c.is_free);
|
||||
assert!(!c.is_unlimited);
|
||||
assert!(!c.is_rate_limited);
|
||||
assert!(!c.has_watermark);
|
||||
assert!(c.failures_are_refunded.is_none());
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
use std::fmt::Debug;
|
||||
use std::sync::Arc;
|
||||
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{
|
||||
enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs, Gemini25FlashEditRequest,
|
||||
};
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{
|
||||
enqueue_gemini_25_flash_text_to_image_webhook, Gemini25FlashTextToImageArgs,
|
||||
Gemini25FlashTextToImageRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_image::generate_image_response::{
|
||||
FalImageResponsePayload, GenerateImageResponse,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum FalNanoBananaRequestState {
|
||||
TextToImage(Gemini25FlashTextToImageRequest),
|
||||
EditImage(Gemini25FlashEditRequest),
|
||||
}
|
||||
|
||||
impl FalNanoBananaRequestState {
|
||||
pub async fn send(&self, client: &RouterFalClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
|
||||
let (webhook_response, outbound): (_, Arc<dyn Debug + Send + Sync>) = match self {
|
||||
Self::TextToImage(request) => {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let args = Gemini25FlashTextToImageArgs {
|
||||
request: request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
(enqueue_gemini_25_flash_text_to_image_webhook(args).await, outbound)
|
||||
}
|
||||
Self::EditImage(request) => {
|
||||
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
|
||||
let args = Gemini25FlashEditArgs {
|
||||
request: request.clone(),
|
||||
webhook_url: client.webhook_url.as_str(),
|
||||
api_key: &client.api_key,
|
||||
};
|
||||
(enqueue_gemini_25_flash_edit_webhook(args).await, outbound)
|
||||
}
|
||||
};
|
||||
|
||||
let webhook_response = webhook_response
|
||||
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
|
||||
|
||||
Ok(GenerateImageResponse::Fal(FalImageResponsePayload {
|
||||
request_id: webhook_response.request_id,
|
||||
gateway_request_id: webhook_response.gateway_request_id,
|
||||
maybe_status_url: None,
|
||||
maybe_response_url: None,
|
||||
maybe_outbound_request: Some(outbound),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::creds::fal_api_key::FalApiKey;
|
||||
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::Gemini25FlashEditNumImages;
|
||||
use fal_client::requests::webhook::image::text::enqueue_gemini_25_flash_text_to_image_webhook::{
|
||||
Gemini25FlashTextToImageAspectRatio, Gemini25FlashTextToImageNumImages,
|
||||
};
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
fn read_fal_api_key() -> FalApiKey {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
|
||||
.expect("Failed to read fal_api_key.txt");
|
||||
FalApiKey::from_str(secret.trim())
|
||||
}
|
||||
|
||||
fn client_with_webhook() -> RouterFalClient {
|
||||
RouterFalClient::new(
|
||||
read_fal_api_key(),
|
||||
"https://example.com/fal-webhook-test".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_t2i_via_webhook() {
|
||||
let client = client_with_webhook();
|
||||
let state = FalNanoBananaRequestState::TextToImage(Gemini25FlashTextToImageRequest {
|
||||
prompt: "a corgi wearing sunglasses on a surfboard".to_string(),
|
||||
num_images: Gemini25FlashTextToImageNumImages::One,
|
||||
aspect_ratio: Some(Gemini25FlashTextToImageAspectRatio::SixteenByNine),
|
||||
});
|
||||
let response = state.send(&client).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("nano_banana t2i — request_id: {:?}", payload.request_id);
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs cost
|
||||
async fn send_edit_via_webhook() {
|
||||
let client = client_with_webhook();
|
||||
let state = FalNanoBananaRequestState::EditImage(Gemini25FlashEditRequest {
|
||||
prompt: "add a party hat to the dog".to_string(),
|
||||
image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()],
|
||||
num_images: Gemini25FlashEditNumImages::One,
|
||||
aspect_ratio: None,
|
||||
});
|
||||
let response = state.send(&client).await.expect("send should succeed");
|
||||
let payload = response.get_fal_payload().expect("expected Fal payload");
|
||||
println!("nano_banana edit — request_id: {:?}", payload.request_id);
|
||||
assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some());
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -122,7 +122,10 @@ fn plan_resolution(resolution: Option<CommonResolution>) -> Option<PlannedResolu
|
||||
CommonResolution::HalfK | CommonResolution::FourEightyP => PlannedResolution::HalfK,
|
||||
CommonResolution::OneK | CommonResolution::SevenTwentyP | CommonResolution::TenEightyP => PlannedResolution::OneK,
|
||||
CommonResolution::TwoK => PlannedResolution::TwoK,
|
||||
CommonResolution::ThreeK | CommonResolution::FourK => PlannedResolution::FourK,
|
||||
// 3K isn't natively supported on Fal nano_banana_2; v1 downgrades to 2K
|
||||
// pricing (and TwoK resolution). Match v1 here so cost+behavior agree.
|
||||
CommonResolution::ThreeK => PlannedResolution::TwoK,
|
||||
CommonResolution::FourK => PlannedResolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -416,13 +419,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_k_rounds_up_to_four_k() {
|
||||
fn three_k_rounds_down_to_two_k() {
|
||||
// Match v1's downgrade behavior: 3K → 2K (cheaper, no 3K native support).
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
resolution: Some(CommonResolution::ThreeK),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana_2(builder));
|
||||
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::FourK)));
|
||||
assert!(matches!(req.resolution, Some(NanoBanana2TextToImageResolution::TwoK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+74
-16
@@ -1,24 +1,44 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
use fal_client::requests::api::image::edit::nano_banana_2_edit_image::api::{
|
||||
NanoBanana2EditImageNumImages, NanoBanana2EditImageResolution,
|
||||
};
|
||||
use fal_client::requests::api::image::text::nano_banana_2_text_to_image::api::{
|
||||
NanoBanana2TextToImageNumImages, NanoBanana2TextToImageResolution,
|
||||
};
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
|
||||
|
||||
/// Cost state for Fal Nano Banana 2. Mirrors v1
|
||||
/// (`estimate_image_cost_fal_nano_banana_2`):
|
||||
///
|
||||
/// $0.08/image base (1K). Multipliers: 0.5K = 0.75x, 2K = 1.5x, 4K = 2x.
|
||||
/// 0.5K → 6¢, 1K → 8¢ (also default), 2K → 12¢, 4K → 16¢
|
||||
///
|
||||
/// The hand-rolled rates here intentionally override fal_client's trait,
|
||||
/// whose rates are placeholder values from a `TODO(bt): Verify pricing` block
|
||||
/// (8/15/15/30 instead of v1's 6/8/12/16). Keep this in sync with the v1
|
||||
/// cost file.
|
||||
pub struct FalNanoBanana2CostState {
|
||||
cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalNanoBanana2CostState {
|
||||
pub fn from_request(request: &FalNanoBanana2RequestState) -> Self {
|
||||
let cost_in_usd_cents = match request {
|
||||
FalNanoBanana2RequestState::TextToImage(req) => req.calculate_cost_in_cents(),
|
||||
FalNanoBanana2RequestState::EditImage(req) => req.calculate_cost_in_cents(),
|
||||
let (cost_per_image, num_images) = match request {
|
||||
FalNanoBanana2RequestState::TextToImage(req) => {
|
||||
(cost_per_image_for_t2i_resolution(req.resolution), t2i_num_images(req.num_images))
|
||||
}
|
||||
FalNanoBanana2RequestState::EditImage(req) => {
|
||||
(cost_per_image_for_edit_resolution(req.resolution), edit_num_images(req.num_images))
|
||||
}
|
||||
};
|
||||
Self { cost_in_usd_cents }
|
||||
Self { cost_in_usd_cents: cost_per_image * num_images }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -29,6 +49,42 @@ impl FalNanoBanana2CostState {
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_per_image_for_t2i_resolution(r: Option<NanoBanana2TextToImageResolution>) -> u64 {
|
||||
match r {
|
||||
Some(NanoBanana2TextToImageResolution::HalfK) => 6,
|
||||
Some(NanoBanana2TextToImageResolution::OneK) | None => 8,
|
||||
Some(NanoBanana2TextToImageResolution::TwoK) => 12,
|
||||
Some(NanoBanana2TextToImageResolution::FourK) => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_per_image_for_edit_resolution(r: Option<NanoBanana2EditImageResolution>) -> u64 {
|
||||
match r {
|
||||
Some(NanoBanana2EditImageResolution::HalfK) => 6,
|
||||
Some(NanoBanana2EditImageResolution::OneK) | None => 8,
|
||||
Some(NanoBanana2EditImageResolution::TwoK) => 12,
|
||||
Some(NanoBanana2EditImageResolution::FourK) => 16,
|
||||
}
|
||||
}
|
||||
|
||||
fn t2i_num_images(n: NanoBanana2TextToImageNumImages) -> u64 {
|
||||
match n {
|
||||
NanoBanana2TextToImageNumImages::One => 1,
|
||||
NanoBanana2TextToImageNumImages::Two => 2,
|
||||
NanoBanana2TextToImageNumImages::Three => 3,
|
||||
NanoBanana2TextToImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_num_images(n: NanoBanana2EditImageNumImages) -> u64 {
|
||||
match n {
|
||||
NanoBanana2EditImageNumImages::One => 1,
|
||||
NanoBanana2EditImageNumImages::Two => 2,
|
||||
NanoBanana2EditImageNumImages::Three => 3,
|
||||
NanoBanana2EditImageNumImages::Four => 4,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -61,7 +117,8 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::One, None),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
|
||||
// None defaults to 1K = 8¢ per v1's rate table.
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -69,7 +126,8 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::One, Some(NanoBanana2TextToImageResolution::HalfK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
// HalfK = 0.75x base = 6¢ per v1's rate table.
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -77,7 +135,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::Four, Some(NanoBanana2TextToImageResolution::OneK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(32));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -85,7 +143,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::One, Some(NanoBanana2TextToImageResolution::FourK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -93,7 +151,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_t2i(NanoBanana2TextToImageNumImages::Two, Some(NanoBanana2TextToImageResolution::FourK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(60));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(32));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +176,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::One, None),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(15));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -126,7 +184,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::One, Some(NanoBanana2EditImageResolution::HalfK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(8));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -134,7 +192,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::Three, Some(NanoBanana2EditImageResolution::TwoK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(45));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(36));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -142,7 +200,7 @@ mod tests {
|
||||
let state = FalNanoBanana2CostState::from_request(
|
||||
&make_edit(NanoBanana2EditImageNumImages::One, Some(NanoBanana2EditImageResolution::FourK)),
|
||||
);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(30));
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents, Some(16));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,6 +219,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
+7
-3
@@ -121,7 +121,10 @@ fn plan_resolution(resolution: Option<CommonResolution>) -> Option<PlannedResolu
|
||||
CommonResolution::HalfK | CommonResolution::FourEightyP | CommonResolution::SevenTwentyP
|
||||
| CommonResolution::OneK | CommonResolution::TenEightyP => PlannedResolution::OneK,
|
||||
CommonResolution::TwoK => PlannedResolution::TwoK,
|
||||
CommonResolution::ThreeK | CommonResolution::FourK => PlannedResolution::FourK,
|
||||
// 3K isn't natively supported on Fal nano_banana_pro; v1 downgrades to 2K
|
||||
// pricing (and TwoK resolution). Match v1 here so cost+behavior agree.
|
||||
CommonResolution::ThreeK => PlannedResolution::TwoK,
|
||||
CommonResolution::FourK => PlannedResolution::FourK,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -393,13 +396,14 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_k_rounds_up_to_four_k() {
|
||||
fn three_k_rounds_down_to_two_k() {
|
||||
// Match v1's downgrade behavior: 3K → 2K (cheaper, no 3K native support).
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
resolution: Some(CommonResolution::ThreeK),
|
||||
..base_builder()
|
||||
};
|
||||
let req = unwrap_t2i(build_fal_nano_banana_pro(builder));
|
||||
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::FourK)));
|
||||
assert!(matches!(req.resolution, Some(NanoBananaProTextToImageResolution::TwoK)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+3
-2
@@ -18,7 +18,8 @@ impl FalNanoBananaProCostState {
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
// v1 sets cost_in_credits to the same value as USD cents (1:1).
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
@@ -145,6 +146,6 @@ mod tests {
|
||||
assert!(!cost.is_unlimited);
|
||||
assert!(!cost.is_rate_limited);
|
||||
assert!(!cost.has_watermark);
|
||||
assert!(cost.cost_in_credits.is_none());
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
use fal_client::requests::webhook::image::angle::enqueue_qwen_edit_2511_edit_image_angle_webhook::{
|
||||
EnqueueQwenEdit2511AngleImageSize, EnqueueQwenEdit2511AngleNumImages,
|
||||
EnqueueQwenEdit2511EditImageAngleRequest,
|
||||
};
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::client_error::ClientError;
|
||||
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
|
||||
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
|
||||
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
|
||||
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::request::FalQwenEdit2511AnglesRequestState;
|
||||
|
||||
pub fn build_fal_qwen_edit_2511_angles(
|
||||
builder: GenerateImageRequestBuilder,
|
||||
) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
let image_urls = resolve_image_urls(builder.image_inputs.clone())?;
|
||||
if image_urls.is_empty() {
|
||||
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_inputs",
|
||||
value: "Angle models require at least one input image URL".to_string(),
|
||||
}));
|
||||
}
|
||||
let num_images = plan_num_images(builder.image_batch_count, strategy)?;
|
||||
let image_size = plan_image_size(builder.aspect_ratio, strategy)?;
|
||||
|
||||
let request = EnqueueQwenEdit2511EditImageAngleRequest {
|
||||
image_urls,
|
||||
horizontal_angle: builder.horizontal_angle,
|
||||
vertical_angle: builder.vertical_angle,
|
||||
zoom: builder.zoom,
|
||||
additional_prompt: builder.prompt.clone(),
|
||||
num_images: Some(num_images),
|
||||
image_size,
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
};
|
||||
|
||||
Ok(ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalQwenEdit2511Angles(FalQwenEdit2511AnglesRequestState { request }),
|
||||
))
|
||||
}
|
||||
|
||||
fn resolve_image_urls(image_inputs: Option<ImageListRef>) -> Result<Vec<String>, ArtcraftRouterError> {
|
||||
match image_inputs {
|
||||
None => Ok(vec![]),
|
||||
Some(ImageListRef::Urls(urls)) => Ok(urls),
|
||||
Some(ImageListRef::MediaFileTokens(_)) => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_num_images(
|
||||
image_batch_count: Option<u16>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<EnqueueQwenEdit2511AngleNumImages, ArtcraftRouterError> {
|
||||
use EnqueueQwenEdit2511AngleNumImages as N;
|
||||
let count = image_batch_count.unwrap_or(1);
|
||||
match count {
|
||||
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
|
||||
1 => Ok(N::One),
|
||||
2 => Ok(N::Two),
|
||||
3 => Ok(N::Three),
|
||||
4 => Ok(N::Four),
|
||||
_ => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "image_batch_count",
|
||||
value: format!("{}", count),
|
||||
}))
|
||||
}
|
||||
_ => Ok(N::Four),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_image_size(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Result<Option<EnqueueQwenEdit2511AngleImageSize>, ArtcraftRouterError> {
|
||||
use EnqueueQwenEdit2511AngleImageSize as S;
|
||||
match aspect_ratio {
|
||||
None => Ok(None),
|
||||
|
||||
Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
|
||||
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
|
||||
|
||||
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
|
||||
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
|
||||
|
||||
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
|
||||
|
||||
Some(unsupported) => match strategy {
|
||||
RequestMismatchMitigationStrategy::ErrorOut => {
|
||||
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
|
||||
field: "aspect_ratio",
|
||||
value: format!("{:?}", unsupported),
|
||||
}))
|
||||
}
|
||||
_ => Ok(Some(S::SquareHd)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::api::common_image_model::CommonImageModel;
|
||||
use crate::api::provider::Provider;
|
||||
|
||||
fn base() -> GenerateImageRequestBuilder {
|
||||
GenerateImageRequestBuilder {
|
||||
model: CommonImageModel::QwenEdit2511Angles,
|
||||
provider: Provider::Fal,
|
||||
prompt: Some("test".to_string()),
|
||||
image_inputs: Some(ImageListRef::Urls(vec!["https://example.com/x.jpg".to_string()])),
|
||||
resolution: None,
|
||||
aspect_ratio: None,
|
||||
quality: None,
|
||||
image_batch_count: None,
|
||||
horizontal_angle: Some(45.0),
|
||||
vertical_angle: Some(-15.0),
|
||||
zoom: Some(2.0),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
|
||||
generation_mode_mismatch_strategy: None,
|
||||
idempotency_token: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn unwrap_request(result: Result<ImageGenerationDraftOrRequest, ArtcraftRouterError>) -> EnqueueQwenEdit2511EditImageAngleRequest {
|
||||
let ImageGenerationDraftOrRequest::Request(
|
||||
ImageGenerationRequest::FalQwenEdit2511Angles(state)
|
||||
) = result.expect("build should succeed") else {
|
||||
panic!("expected FalQwenEdit2511Angles variant")
|
||||
};
|
||||
state.request
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn passes_through_camera_params() {
|
||||
let req = unwrap_request(build_fal_qwen_edit_2511_angles(base()));
|
||||
assert_eq!(req.horizontal_angle, Some(45.0));
|
||||
assert_eq!(req.vertical_angle, Some(-15.0));
|
||||
assert_eq!(req.zoom, Some(2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_image_inputs_errors() {
|
||||
let builder = GenerateImageRequestBuilder { image_inputs: None, ..base() };
|
||||
assert!(build_fal_qwen_edit_2511_angles(builder).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_file_tokens_rejected() {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_inputs: Some(ImageListRef::MediaFileTokens(vec![MediaFileToken::new_from_str("mf_test")])),
|
||||
..base()
|
||||
};
|
||||
assert!(matches!(
|
||||
build_fal_qwen_edit_2511_angles(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_num_images_is_one() {
|
||||
let req = unwrap_request(build_fal_qwen_edit_2511_angles(base()));
|
||||
assert!(matches!(req.num_images, Some(EnqueueQwenEdit2511AngleNumImages::One)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_zero_errors() {
|
||||
let builder = GenerateImageRequestBuilder { image_batch_count: Some(0), ..base() };
|
||||
assert!(matches!(
|
||||
build_fal_qwen_edit_2511_angles(builder),
|
||||
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_over_four_clamps_with_upgrade() {
|
||||
let builder = GenerateImageRequestBuilder {
|
||||
image_batch_count: Some(9),
|
||||
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
|
||||
..base()
|
||||
};
|
||||
let req = unwrap_request(build_fal_qwen_edit_2511_angles(builder));
|
||||
assert!(matches!(req.num_images, Some(EnqueueQwenEdit2511AngleNumImages::Four)));
|
||||
}
|
||||
}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
use fal_client::requests::traits::fal_request_cost_calculator_trait::FalRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
|
||||
use crate::generate::generate_image_v2::providers::fal::qwen_edit_2511_angles::request::FalQwenEdit2511AnglesRequestState;
|
||||
|
||||
/// Cost state for Fal Qwen Edit 2511 Angles. Delegates to the fal_client
|
||||
/// cost calculator (4¢ per output image — $0.035/MP at the default ~1MP).
|
||||
pub struct FalQwenEdit2511AnglesCostState {
|
||||
cost_in_usd_cents: u64,
|
||||
}
|
||||
|
||||
impl FalQwenEdit2511AnglesCostState {
|
||||
pub fn from_request(request: &FalQwenEdit2511AnglesRequestState) -> Self {
|
||||
Self {
|
||||
cost_in_usd_cents: request.request.calculate_cost_in_cents(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> ImageGenerationCostEstimate {
|
||||
ImageGenerationCostEstimate {
|
||||
cost_in_credits: Some(self.cost_in_usd_cents),
|
||||
cost_in_usd_cents: Some(self.cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use fal_client::requests::webhook::image::angle::enqueue_qwen_edit_2511_edit_image_angle_webhook::{
|
||||
EnqueueQwenEdit2511AngleNumImages, EnqueueQwenEdit2511EditImageAngleRequest,
|
||||
};
|
||||
|
||||
fn cost_for(n: EnqueueQwenEdit2511AngleNumImages) -> ImageGenerationCostEstimate {
|
||||
let state = FalQwenEdit2511AnglesCostState::from_request(&FalQwenEdit2511AnglesRequestState {
|
||||
request: EnqueueQwenEdit2511EditImageAngleRequest {
|
||||
image_urls: vec!["https://example.com/x.jpg".to_string()],
|
||||
horizontal_angle: None,
|
||||
vertical_angle: None,
|
||||
zoom: None,
|
||||
additional_prompt: None,
|
||||
num_images: Some(n),
|
||||
image_size: None,
|
||||
lora_scale: None,
|
||||
guidance_scale: None,
|
||||
num_inference_steps: None,
|
||||
},
|
||||
});
|
||||
state.estimate_cost()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_image_is_4_cents() {
|
||||
assert_eq!(cost_for(EnqueueQwenEdit2511AngleNumImages::One).cost_in_usd_cents, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_images_is_8_cents() {
|
||||
assert_eq!(cost_for(EnqueueQwenEdit2511AngleNumImages::Two).cost_in_usd_cents, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_images_is_12_cents() {
|
||||
assert_eq!(cost_for(EnqueueQwenEdit2511AngleNumImages::Three).cost_in_usd_cents, Some(12));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn four_images_is_16_cents() {
|
||||
assert_eq!(cost_for(EnqueueQwenEdit2511AngleNumImages::Four).cost_in_usd_cents, Some(16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn credits_equal_cents() {
|
||||
let cost = cost_for(EnqueueQwenEdit2511AngleNumImages::Two);
|
||||
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
Loaded 100 of 170 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user