From 46b3c92c8b84864e077a4da111a65fdcbbcf0b59 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 26 May 2026 18:10:30 -0400 Subject: [PATCH] artcraft router for images - pipeline v2 (#1580) --- ...mni_gen_image_cost_and_generate_request.rs | 2 +- .../generate_image_request_builder.rs | 73 ++++ .../image_generation_request.rs | 266 +++++++++++- .../providers/artcraft/angle_validation.rs | 27 ++ .../providers/artcraft/build_common.rs | 136 ++++++ .../providers/artcraft/cost_parity_tests.rs | 318 ++++++++++++++ .../providers/artcraft/flux_1_dev/build.rs | 17 + .../providers/artcraft/flux_1_dev/cost.rs | 72 ++++ .../providers/artcraft/flux_1_dev/mod.rs | 3 + .../providers/artcraft/flux_1_dev/request.rs | 17 + .../artcraft/flux_1_schnell/build.rs | 17 + .../providers/artcraft/flux_1_schnell/cost.rs | 71 ++++ .../providers/artcraft/flux_1_schnell/mod.rs | 3 + .../artcraft/flux_1_schnell/request.rs | 17 + .../artcraft/flux_2_lora_angles/build.rs | 19 + .../artcraft/flux_2_lora_angles/cost.rs | 73 ++++ .../artcraft/flux_2_lora_angles/mod.rs | 3 + .../artcraft/flux_2_lora_angles/request.rs | 17 + .../providers/artcraft/flux_pro_1p1/build.rs | 17 + .../providers/artcraft/flux_pro_1p1/cost.rs | 71 ++++ .../providers/artcraft/flux_pro_1p1/mod.rs | 3 + .../artcraft/flux_pro_1p1/request.rs | 17 + .../artcraft/flux_pro_1p1_ultra/build.rs | 17 + .../artcraft/flux_pro_1p1_ultra/cost.rs | 70 ++++ .../artcraft/flux_pro_1p1_ultra/mod.rs | 3 + .../artcraft/flux_pro_1p1_ultra/request.rs | 17 + .../providers/artcraft/gpt_image_1/build.rs | 17 + .../providers/artcraft/gpt_image_1/cost.rs | 194 +++++++++ .../providers/artcraft/gpt_image_1/mod.rs | 3 + .../providers/artcraft/gpt_image_1/request.rs | 17 + .../providers/artcraft/gpt_image_1p5/build.rs | 17 + .../providers/artcraft/gpt_image_1p5/cost.rs | 173 ++++++++ .../providers/artcraft/gpt_image_1p5/mod.rs | 3 + .../artcraft/gpt_image_1p5/request.rs | 17 + .../providers/artcraft/gpt_image_2/build.rs | 17 + .../providers/artcraft/gpt_image_2/cost.rs | 201 +++++++++ .../providers/artcraft/gpt_image_2/mod.rs | 3 + .../providers/artcraft/gpt_image_2/request.rs | 17 + .../providers/artcraft/mod.rs | 21 + .../providers/artcraft/nano_banana/build.rs | 17 + .../providers/artcraft/nano_banana/cost.rs | 70 ++++ .../providers/artcraft/nano_banana/mod.rs | 3 + .../providers/artcraft/nano_banana/request.rs | 17 + .../providers/artcraft/nano_banana_2/build.rs | 17 + .../providers/artcraft/nano_banana_2/cost.rs | 123 ++++++ .../providers/artcraft/nano_banana_2/mod.rs | 3 + .../artcraft/nano_banana_2/request.rs | 17 + .../artcraft/nano_banana_pro/build.rs | 17 + .../artcraft/nano_banana_pro/cost.rs | 109 +++++ .../providers/artcraft/nano_banana_pro/mod.rs | 3 + .../artcraft/nano_banana_pro/request.rs | 17 + .../artcraft/qwen_edit_2511_angles/build.rs | 19 + .../artcraft/qwen_edit_2511_angles/cost.rs | 72 ++++ .../artcraft/qwen_edit_2511_angles/mod.rs | 3 + .../artcraft/qwen_edit_2511_angles/request.rs | 17 + .../providers/artcraft/request_common.rs | 28 ++ .../providers/artcraft/seedream_4/build.rs | 17 + .../providers/artcraft/seedream_4/cost.rs | 70 ++++ .../providers/artcraft/seedream_4/mod.rs | 3 + .../providers/artcraft/seedream_4/request.rs | 17 + .../providers/artcraft/seedream_4p5/build.rs | 17 + .../providers/artcraft/seedream_4p5/cost.rs | 64 +++ .../providers/artcraft/seedream_4p5/mod.rs | 3 + .../artcraft/seedream_4p5/request.rs | 17 + .../artcraft/seedream_5_lite/build.rs | 17 + .../artcraft/seedream_5_lite/cost.rs | 64 +++ .../providers/artcraft/seedream_5_lite/mod.rs | 3 + .../artcraft/seedream_5_lite/request.rs | 17 + .../providers/fal/cost_parity_tests.rs | 266 ++++++++++++ .../providers/fal/flux_1_dev/build.rs | 20 +- .../providers/fal/flux_1_dev/cost.rs | 7 +- .../providers/fal/flux_1_schnell/build.rs | 20 +- .../providers/fal/flux_1_schnell/cost.rs | 5 +- .../providers/fal/flux_2_lora_angles/build.rs | 184 +++++++++ .../providers/fal/flux_2_lora_angles/cost.rs | 75 ++++ .../providers/fal/flux_2_lora_angles/mod.rs | 3 + .../fal/flux_2_lora_angles/request.rs | 85 ++++ .../providers/fal/flux_pro_1p1/build.rs | 303 ++++++++++++++ .../providers/fal/flux_pro_1p1/cost.rs | 86 ++++ .../providers/fal/flux_pro_1p1/mod.rs | 3 + .../providers/fal/flux_pro_1p1/request.rs | 82 ++++ .../providers/fal/flux_pro_1p1_ultra/build.rs | 283 +++++++++++++ .../providers/fal/flux_pro_1p1_ultra/cost.rs | 85 ++++ .../providers/fal/flux_pro_1p1_ultra/mod.rs | 3 + .../fal/flux_pro_1p1_ultra/request.rs | 82 ++++ .../providers/fal/gpt_image_1/cost.rs | 137 +++++- .../providers/fal/gpt_image_1p5/cost.rs | 133 +++++- .../providers/fal/gpt_image_2/cost.rs | 328 ++++++++------- .../generate_image_v2/providers/fal/mod.rs | 11 + .../providers/fal/nano_banana/build.rs | 389 ++++++++++++++++++ .../providers/fal/nano_banana/cost.rs | 118 ++++++ .../providers/fal/nano_banana/mod.rs | 3 + .../providers/fal/nano_banana/request.rs | 114 +++++ .../providers/fal/nano_banana_2/build.rs | 10 +- .../providers/fal/nano_banana_2/cost.rs | 90 +++- .../providers/fal/nano_banana_pro/build.rs | 10 +- .../providers/fal/nano_banana_pro/cost.rs | 5 +- .../fal/qwen_edit_2511_angles/build.rs | 202 +++++++++ .../fal/qwen_edit_2511_angles/cost.rs | 82 ++++ .../fal/qwen_edit_2511_angles/mod.rs | 3 + .../fal/qwen_edit_2511_angles/request.rs | 86 ++++ .../providers/fal/seedream_4/build.rs | 375 +++++++++++++++++ .../providers/fal/seedream_4/cost.rs | 124 ++++++ .../providers/fal/seedream_4/mod.rs | 3 + .../providers/fal/seedream_4/request.rs | 115 ++++++ .../providers/fal/seedream_4p5/build.rs | 286 +++++++++++++ .../providers/fal/seedream_4p5/cost.rs | 106 +++++ .../providers/fal/seedream_4p5/mod.rs | 3 + .../providers/fal/seedream_4p5/request.rs | 96 +++++ .../providers/fal/seedream_5_lite/build.rs | 309 ++++++++++++++ .../providers/fal/seedream_5_lite/cost.rs | 106 +++++ .../providers/fal/seedream_5_lite/mod.rs | 3 + .../providers/fal/seedream_5_lite/request.rs | 96 +++++ .../generate_image_v2/providers/mod.rs | 1 + .../providers/artcraft/build_common.rs | 2 +- .../artcraft/grok_imagine_video/build.rs | 4 +- .../artcraft/grok_imagine_video/request.rs | 4 +- .../artcraft/happy_horse_1p0/build.rs | 4 +- .../artcraft/happy_horse_1p0/request.rs | 4 +- .../providers/artcraft/kling_1_6_pro/build.rs | 4 +- .../artcraft/kling_1_6_pro/request.rs | 4 +- .../artcraft/kling_2_1_master/build.rs | 4 +- .../artcraft/kling_2_1_master/request.rs | 4 +- .../providers/artcraft/kling_2_1_pro/build.rs | 4 +- .../artcraft/kling_2_1_pro/request.rs | 4 +- .../artcraft/kling_2_5_turbo_pro/build.rs | 4 +- .../artcraft/kling_2_5_turbo_pro/request.rs | 4 +- .../providers/artcraft/kling_2_6_pro/build.rs | 4 +- .../artcraft/kling_2_6_pro/request.rs | 4 +- .../providers/artcraft/kling_3p0_pro/build.rs | 4 +- .../artcraft/kling_3p0_pro/request.rs | 4 +- .../artcraft/kling_3p0_standard/build.rs | 4 +- .../artcraft/kling_3p0_standard/request.rs | 4 +- .../providers/artcraft/preview_model/build.rs | 4 +- .../artcraft/preview_model/request.rs | 4 +- .../artcraft/preview_model_fast/build.rs | 4 +- .../artcraft/preview_model_fast/request.rs | 4 +- .../providers/artcraft/request_common.rs | 2 +- .../artcraft/seedance_1p0_lite/build.rs | 4 +- .../artcraft/seedance_1p0_lite/request.rs | 4 +- .../artcraft/seedance_1p5_pro/build.rs | 4 +- .../artcraft/seedance_1p5_pro/request.rs | 4 +- .../providers/artcraft/seedance_2p0/build.rs | 4 +- .../artcraft/seedance_2p0/request.rs | 4 +- .../artcraft/seedance_2p0_bp/build.rs | 4 +- .../artcraft/seedance_2p0_bp/request.rs | 4 +- .../artcraft/seedance_2p0_bp_fast/build.rs | 4 +- .../artcraft/seedance_2p0_bp_fast/request.rs | 4 +- .../artcraft/seedance_2p0_fast/build.rs | 4 +- .../artcraft/seedance_2p0_fast/request.rs | 4 +- .../artcraft/seedance_2p0_u/build.rs | 4 +- .../artcraft/seedance_2p0_u/request.rs | 4 +- .../artcraft/seedance_2p0_u_fast/build.rs | 4 +- .../artcraft/seedance_2p0_u_fast/request.rs | 4 +- .../providers/artcraft/sora_2/build.rs | 4 +- .../providers/artcraft/sora_2/request.rs | 4 +- .../providers/artcraft/sora_2_pro/build.rs | 4 +- .../providers/artcraft/sora_2_pro/request.rs | 4 +- .../providers/artcraft/veo_2/build.rs | 4 +- .../providers/artcraft/veo_2/request.rs | 4 +- .../providers/artcraft/veo_3/build.rs | 4 +- .../providers/artcraft/veo_3/request.rs | 4 +- .../providers/artcraft/veo_3_fast/build.rs | 4 +- .../providers/artcraft/veo_3_fast/request.rs | 4 +- .../providers/artcraft/veo_3p1/build.rs | 4 +- .../providers/artcraft/veo_3p1/request.rs | 4 +- .../providers/artcraft/veo_3p1_fast/build.rs | 4 +- .../artcraft/veo_3p1_fast/request.rs | 4 +- .../common/generation/common_image_model.rs | 12 +- .../image/estimate_image_cost_handler.rs | 2 + 170 files changed, 7762 insertions(+), 327 deletions(-) create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/angle_validation.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/build_common.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/cost_parity_tests.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/request_common.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/cost_parity_tests.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/request.rs diff --git a/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_image_cost_and_generate_request.rs b/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_image_cost_and_generate_request.rs index dd21ef4514..38399e2b6f 100644 --- a/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_image_cost_and_generate_request.rs +++ b/crates/api_clients/artcraft/artcraft_api_defs/src/omni_gen/cost_and_generate_requests/omni_gen_image_cost_and_generate_request.rs @@ -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. diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs index 166f4db952..5451ac2be4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs @@ -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 { 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(), } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/image_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/image_generation_request.rs index 7c7135cbfd..b61d0ba26a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/image_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/image_generation_request.rs @@ -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 { 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 { 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 + } } } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/angle_validation.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/angle_validation.rs new file mode 100644 index 0000000000..d3a5588e0a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/angle_validation.rs @@ -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, +) -> 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(()), + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/build_common.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/build_common.rs new file mode 100644 index 0000000000..0a50b72b8d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/build_common.rs @@ -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 { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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` 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, +) -> Result>, 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, + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/cost_parity_tests.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/cost_parity_tests.rs new file mode 100644 index 0000000000..228d25b371 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/cost_parity_tests.rs @@ -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] = &[ + None, + Some(1), + Some(2), + Some(3), + Some(4), +]; + +fn all_aspect_ratios() -> &'static [Option] { + &[ + 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] { + &[ + 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] { + &[ + 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> { + 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> { + 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] = &[None, Some(-45.0), Some(0.0), Some(45.0)]; + const ZOOMS: &[Option] = &[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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/build.rs new file mode 100644 index 0000000000..62f49bcd99 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Flux1Dev)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftFlux1Dev(ArtcraftFlux1DevRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/cost.rs new file mode 100644 index 0000000000..e92cd0cb0d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/request.rs new file mode 100644 index 0000000000..6c08b960b3 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_dev/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/build.rs new file mode 100644 index 0000000000..c4e686fa2b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Flux1Schnell)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftFlux1Schnell(ArtcraftFlux1SchnellRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/cost.rs new file mode 100644 index 0000000000..dfe0c221b9 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/cost.rs @@ -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); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/request.rs new file mode 100644 index 0000000000..c58a61ddb8 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_1_schnell/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/build.rs new file mode 100644 index 0000000000..966ba3b725 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/build.rs @@ -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 { + 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 }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/cost.rs new file mode 100644 index 0000000000..15dc60454d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/request.rs new file mode 100644 index 0000000000..f6eb078dfb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_2_lora_angles/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/build.rs new file mode 100644 index 0000000000..a91e3ff0b6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::FluxPro11)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftFluxPro1p1(ArtcraftFluxPro1p1RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/cost.rs new file mode 100644 index 0000000000..fd11ed1536 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/request.rs new file mode 100644 index 0000000000..d5e5cbe498 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/build.rs new file mode 100644 index 0000000000..755cca881a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::FluxPro11Ultra)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftFluxPro1p1Ultra(ArtcraftFluxPro1p1UltraRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/cost.rs new file mode 100644 index 0000000000..93d0adcf10 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/request.rs new file mode 100644 index 0000000000..5169ffcbe0 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/flux_pro_1p1_ultra/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/build.rs new file mode 100644 index 0000000000..66f016c82a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::GptImage1)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftGptImage1(ArtcraftGptImage1RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/cost.rs new file mode 100644 index 0000000000..927122a3c6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/cost.rs @@ -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, + pub aspect_ratio: Option, + 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) -> 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, aspect_ratio: Option, 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, aspect_ratio: Option, batch: u16, num_inputs: usize) -> u64 { + let tokens: Vec = (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); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/request.rs new file mode 100644 index 0000000000..6e296997aa --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/build.rs new file mode 100644 index 0000000000..996730388f --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::GptImage1p5)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftGptImage1p5(ArtcraftGptImage1p5RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/cost.rs new file mode 100644 index 0000000000..315e48b97b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/cost.rs @@ -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, + pub aspect_ratio: Option, + 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) -> 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, aspect_ratio: Option, 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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/request.rs new file mode 100644 index 0000000000..ff44b69975 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_1p5/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/build.rs new file mode 100644 index 0000000000..93fb3ce6d7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::GptImage2)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftGptImage2(ArtcraftGptImage2RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/cost.rs new file mode 100644 index 0000000000..d7c3f1d240 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/cost.rs @@ -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, + pub aspect_ratio: Option, + 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) -> 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, aspect_ratio: Option, 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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/request.rs new file mode 100644 index 0000000000..179c113ec9 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/gpt_image_2/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/mod.rs new file mode 100644 index 0000000000..e39ad1d81e --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/mod.rs @@ -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; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/build.rs new file mode 100644 index 0000000000..0d1db0e475 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::NanoBanana)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftNanoBanana(ArtcraftNanoBananaRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/cost.rs new file mode 100644 index 0000000000..bd640cd659 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/request.rs new file mode 100644 index 0000000000..51d5cda174 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/build.rs new file mode 100644 index 0000000000..15a6d23479 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::NanoBanana2)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftNanoBanana2(ArtcraftNanoBanana2RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/cost.rs new file mode 100644 index 0000000000..d77bbdb7a3 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/cost.rs @@ -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, + 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) -> 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, 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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/request.rs new file mode 100644 index 0000000000..bbee1b27c3 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_2/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/build.rs new file mode 100644 index 0000000000..50c793e55b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::NanoBananaPro)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftNanoBananaPro(ArtcraftNanoBananaProRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/cost.rs new file mode 100644 index 0000000000..20770addbb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/cost.rs @@ -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, + 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, 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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/request.rs new file mode 100644 index 0000000000..86bbdaaea6 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/nano_banana_pro/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/build.rs new file mode 100644 index 0000000000..d89dfde967 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/build.rs @@ -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 { + 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 }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/cost.rs new file mode 100644 index 0000000000..6100c4163c --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/request.rs new file mode 100644 index 0000000000..43fbdeef83 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/qwen_edit_2511_angles/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/request_common.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/request_common.rs new file mode 100644 index 0000000000..8b9abd661a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/request_common.rs @@ -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 { + 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, + })) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/build.rs new file mode 100644 index 0000000000..9940c7775a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Seedream4)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftSeedream4(ArtcraftSeedream4RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/cost.rs new file mode 100644 index 0000000000..0bb3dec047 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/request.rs new file mode 100644 index 0000000000..971de9c739 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/build.rs new file mode 100644 index 0000000000..e9c63faac8 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Seedream4p5)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftSeedream4p5(ArtcraftSeedream4p5RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/cost.rs new file mode 100644 index 0000000000..2004d3b390 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/request.rs new file mode 100644 index 0000000000..a616231783 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_4p5/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/build.rs new file mode 100644 index 0000000000..7a2a903afb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/build.rs @@ -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 { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Seedream5Lite)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftSeedream5Lite(ArtcraftSeedream5LiteRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/cost.rs new file mode 100644 index 0000000000..2202323c4c --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/cost.rs @@ -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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/request.rs new file mode 100644 index 0000000000..fe173fd3d5 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/artcraft/seedream_5_lite/request.rs @@ -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 { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/cost_parity_tests.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/cost_parity_tests.rs new file mode 100644 index 0000000000..83fc96e4c3 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/cost_parity_tests.rs @@ -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] = &[ + None, + Some(1), + Some(2), + Some(3), + Some(4), +]; + +fn all_aspect_ratios() -> &'static [Option] { + &[ + 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] { + &[ + 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] { + &[ + 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> { + 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); } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/build.rs index 12c8f591fa..0527101a88 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/build.rs @@ -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, ) -> Result, 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] diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/cost.rs index 6d1924659e..68cb64f1be 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_dev/cost.rs @@ -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); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/build.rs index 4c27463623..fa956f54e1 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/build.rs @@ -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, ) -> Result, 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] diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/cost.rs index 7a718f408b..c92632595e 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_1_schnell/cost.rs @@ -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); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/build.rs new file mode 100644 index 0000000000..79120065bd --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/build.rs @@ -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 { + 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) -> Result, 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result, 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) -> 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)) + )); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/cost.rs new file mode 100644 index 0000000000..1c8c0c0af4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/cost.rs @@ -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)); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/request.rs new file mode 100644 index 0000000000..bafac2cf3b --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_2_lora_angles/request.rs @@ -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 { + let outbound: Arc = 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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/build.rs new file mode 100644 index 0000000000..eb5bda88b7 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/build.rs @@ -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 { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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) -> 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)); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/cost.rs new file mode 100644 index 0000000000..69b4e7e1ea --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/cost.rs @@ -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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/request.rs new file mode 100644 index 0000000000..fe68fc7156 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1/request.rs @@ -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 { + let outbound: Arc = 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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/build.rs new file mode 100644 index 0000000000..74179da342 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/build.rs @@ -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 { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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) -> 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)); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/cost.rs new file mode 100644 index 0000000000..6b9c8a388d --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/cost.rs @@ -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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/request.rs new file mode 100644 index 0000000000..0d9f03b28c --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/flux_pro_1p1_ultra/request.rs @@ -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 { + let outbound: Arc = 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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1/cost.rs index 4160c43446..57b5027935 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1/cost.rs @@ -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, + image_size: Option, + 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, + image_size: Option, + 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, Option, [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, Option, [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); diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1p5/cost.rs index 979ad06321..8d29823ffc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1p5/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_1p5/cost.rs @@ -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, + image_size: Option, + 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, + image_size: Option, + 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, Option, [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, Option, [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); diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_2/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_2/cost.rs index d9c90a54db..d60b3d84c2 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_2/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/gpt_image_2/cost.rs @@ -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, + image_size: Option, + 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, + image_size: Option, + 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, Option, Option, [u64; 4]); - type EditPriceCase = (Option, Option, Option, [u64; 4]); + type TextPriceCase = (Option, Option, [u64; 4]); + type EditPriceCase = (Option, Option, [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 { - 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 { - 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 { 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); diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/mod.rs index 1267b296b8..f222519e13 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/mod.rs @@ -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; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/build.rs new file mode 100644 index 0000000000..77ecb810da --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/build.rs @@ -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 { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result, 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result, 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, +) -> Result, 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) -> 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) -> 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)); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/cost.rs new file mode 100644 index 0000000000..10eef20a5c --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/cost.rs @@ -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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/request.rs new file mode 100644 index 0000000000..25ab532c52 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana/request.rs @@ -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 { + let (webhook_response, outbound): (_, Arc) = match self { + Self::TextToImage(request) => { + let outbound: Arc = 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 = 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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/build.rs index c898022b74..5f1ebc621c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/build.rs @@ -122,7 +122,10 @@ fn plan_resolution(resolution: Option) -> Option 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] diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/cost.rs index 86380af135..2bf011c781 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_2/cost.rs @@ -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) -> 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) -> 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); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/build.rs index e506be5301..17b4a4f810 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/build.rs @@ -121,7 +121,10 @@ fn plan_resolution(resolution: Option) -> Option 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] diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/cost.rs index c8c8c44dd3..f1bf3afc7b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/nano_banana_pro/cost.rs @@ -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); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/build.rs new file mode 100644 index 0000000000..0af7e2a8e4 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/build.rs @@ -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 { + 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) -> Result, 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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, + strategy: RequestMismatchMitigationStrategy, +) -> Result, 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) -> 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))); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/cost.rs new file mode 100644 index 0000000000..fd19cd67dc --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/cost.rs @@ -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); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/request.rs new file mode 100644 index 0000000000..4a4f8946b1 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/qwen_edit_2511_angles/request.rs @@ -0,0 +1,86 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::image::angle::enqueue_qwen_edit_2511_edit_image_angle_webhook::{ + enqueue_qwen_edit_2511_edit_image_angle_webhook, EnqueueQwenEdit2511EditImageAngleArgs, + EnqueueQwenEdit2511EditImageAngleRequest, +}; + +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 FalQwenEdit2511AnglesRequestState { + pub request: EnqueueQwenEdit2511EditImageAngleRequest, +} + +impl FalQwenEdit2511AnglesRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let outbound: Arc = Arc::new(self.request.clone()); + let args = EnqueueQwenEdit2511EditImageAngleArgs { + request: self.request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + let webhook_response = enqueue_qwen_edit_2511_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_qwen_edit_2511_edit_image_angle_webhook::{ + EnqueueQwenEdit2511AngleImageSize, EnqueueQwenEdit2511AngleNumImages, + }; + + 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 = FalQwenEdit2511AnglesRequestState { + request: EnqueueQwenEdit2511EditImageAngleRequest { + image_urls: vec!["https://example.com/test.jpg".to_string()], + horizontal_angle: Some(45.0), + vertical_angle: Some(-15.0), + zoom: Some(2.0), + additional_prompt: Some("cinematic".to_string()), + num_images: Some(EnqueueQwenEdit2511AngleNumImages::One), + image_size: Some(EnqueueQwenEdit2511AngleImageSize::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()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/build.rs new file mode 100644 index 0000000000..e7f71c8d1e --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/build.rs @@ -0,0 +1,375 @@ +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{ + EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageRequest, + EnqueueBytedanceSeedreamV4EditImageSize, +}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{ + EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageRequest, + EnqueueBytedanceSeedreamV4TextToImageSize, +}; + +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::seedream_4::request::FalSeedream4RequestState; + +pub fn build_fal_seedream_4( + builder: GenerateImageRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + let image_urls = resolve_image_list_ref(builder.image_inputs.clone())?; + let is_edit_mode = !image_urls.is_empty(); + let image_size = plan_image_size(builder.aspect_ratio, is_edit_mode, strategy)?; + let num_images = plan_num_images(builder.image_batch_count, strategy)?; + let prompt = builder.prompt.clone().unwrap_or_default(); + + let state = if is_edit_mode { + FalSeedream4RequestState::EditImage(EnqueueBytedanceSeedreamV4EditImageRequest { + prompt, + image_urls, + num_images: Some(to_edit_num_images(num_images)), + max_images: None, + image_size: image_size.map(to_edit_image_size), + }) + } else { + FalSeedream4RequestState::TextToImage(EnqueueBytedanceSeedreamV4TextToImageRequest { + prompt, + num_images: Some(to_t2i_num_images(num_images)), + max_images: None, + image_size: image_size.map(to_t2i_image_size), + }) + }; + + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream4(state), + )) +} + +// ── Num images ── + +#[derive(Copy, Clone, Debug)] +enum PlannedNumImages { + One, + Two, + Three, + Four, +} + +fn plan_num_images( + count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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) -> EnqueueBytedanceSeedreamV4TextToImageNumImages { + use EnqueueBytedanceSeedreamV4TextToImageNumImages as T; + match n { + PlannedNumImages::One => T::One, + PlannedNumImages::Two => T::Two, + PlannedNumImages::Three => T::Three, + PlannedNumImages::Four => T::Four, + } +} + +fn to_edit_num_images(n: PlannedNumImages) -> EnqueueBytedanceSeedreamV4EditImageNumImages { + use EnqueueBytedanceSeedreamV4EditImageNumImages as E; + match n { + PlannedNumImages::One => E::One, + PlannedNumImages::Two => E::Two, + PlannedNumImages::Three => E::Three, + PlannedNumImages::Four => E::Four, + } +} + +// ── Image size ── + +#[derive(Copy, Clone, Debug)] +enum PlannedImageSize { + Square, + SquareHd, + PortraitFourThree, + PortraitSixteenNine, + LandscapeFourThree, + LandscapeSixteenNine, + Auto, + Auto2k, + Auto4k, +} + +fn plan_image_size( + aspect_ratio: Option, + is_edit_mode: bool, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlannedImageSize as S; + match aspect_ratio { + None => Ok(None), + + Some(CommonAspectRatio::Auto) => { + if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) } + } + Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)), + Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)), + + 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(unsupported @ CommonAspectRatio::WideFiveByFour) + | Some(unsupported @ CommonAspectRatio::WideThreeByTwo) + | Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(S::LandscapeSixteenNine)), + }, + + Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)), + + Some(unsupported @ CommonAspectRatio::TallFourByFive) + | Some(unsupported @ CommonAspectRatio::TallTwoByThree) + | Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(S::PortraitSixteenNine)), + }, + } +} + +fn to_t2i_image_size(s: PlannedImageSize) -> EnqueueBytedanceSeedreamV4TextToImageSize { + use EnqueueBytedanceSeedreamV4TextToImageSize as T; + match s { + PlannedImageSize::Square => T::Square, + PlannedImageSize::SquareHd => T::SquareHd, + PlannedImageSize::PortraitFourThree => T::PortraitFourThree, + PlannedImageSize::PortraitSixteenNine => T::PortraitSixteenNine, + PlannedImageSize::LandscapeFourThree => T::LandscapeFourThree, + PlannedImageSize::LandscapeSixteenNine => T::LandscapeSixteenNine, + PlannedImageSize::Auto => T::Auto, + PlannedImageSize::Auto2k => T::Auto2k, + PlannedImageSize::Auto4k => T::Auto4k, + } +} + +fn to_edit_image_size(s: PlannedImageSize) -> EnqueueBytedanceSeedreamV4EditImageSize { + use EnqueueBytedanceSeedreamV4EditImageSize as E; + match s { + PlannedImageSize::Square => E::Square, + PlannedImageSize::SquareHd => E::SquareHd, + PlannedImageSize::PortraitFourThree => E::PortraitFourThree, + PlannedImageSize::PortraitSixteenNine => E::PortraitSixteenNine, + PlannedImageSize::LandscapeFourThree => E::LandscapeFourThree, + PlannedImageSize::LandscapeSixteenNine => E::LandscapeSixteenNine, + PlannedImageSize::Auto => E::Auto, + PlannedImageSize::Auto2k => E::Auto2k, + PlannedImageSize::Auto4k => E::Auto4k, + } +} + +// ── Image inputs ── + +fn resolve_image_list_ref( + image_list_ref: Option, +) -> Result, 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::Seedream4, + 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) -> EnqueueBytedanceSeedreamV4TextToImageRequest { + let ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream4(FalSeedream4RequestState::TextToImage(req)) + ) = result.expect("build should succeed") else { panic!("expected TextToImage variant") }; + req + } + + fn unwrap_edit(result: Result) -> EnqueueBytedanceSeedreamV4EditImageRequest { + let ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream4(FalSeedream4RequestState::EditImage(req)) + ) = result.expect("build should succeed") else { panic!("expected EditImage variant") }; + req + } + + // ── Mode detection ── + + #[test] + fn no_image_inputs_is_t2i() { + let req = unwrap_t2i(build_fal_seedream_4(base_builder())); + assert_eq!(req.prompt, "a cat in space"); + } + + #[test] + fn image_inputs_is_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_seedream_4(builder)); + assert_eq!(req.image_urls, vec!["https://example.com/img.jpg".to_string()]); + } + + #[test] + fn media_tokens_return_error() { + let builder = GenerateImageRequestBuilder { + image_inputs: Some(ImageListRef::MediaFileTokens(vec![])), + ..base_builder() + }; + assert!(matches!( + build_fal_seedream_4(builder), + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + )); + } + + // ── Image size ── + + #[test] + fn t2i_default_size_is_none() { + let req = unwrap_t2i(build_fal_seedream_4(base_builder())); + assert!(req.image_size.is_none()); + } + + #[test] + fn t2i_auto_maps_to_square() { + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::Auto), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_4(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV4TextToImageSize::Square))); + } + + #[test] + fn edit_auto_stays_auto() { + 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_seedream_4(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV4EditImageSize::Auto))); + } + + #[test] + fn auto4k_maps_to_auto4k() { + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::Auto4k), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_4(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV4TextToImageSize::Auto4k))); + } + + #[test] + fn wide_21_9_errors_out() { + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine), + ..base_builder() + }; + assert!(matches!( + build_fal_seedream_4(builder), + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "aspect_ratio", .. })) + )); + } + + #[test] + fn wide_21_9_falls_back_with_upgrade() { + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::WideTwentyOneByNine), + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade, + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_4(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV4TextToImageSize::LandscapeSixteenNine))); + } + + // ── Num images ── + + #[test] + fn default_num_images_is_one() { + let req = unwrap_t2i(build_fal_seedream_4(base_builder())); + assert!(matches!(req.num_images, Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::One))); + } + + #[test] + fn zero_batch_count_is_error() { + let builder = GenerateImageRequestBuilder { + image_batch_count: Some(0), + ..base_builder() + }; + assert!(matches!( + build_fal_seedream_4(builder), + Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)) + )); + } + + #[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_seedream_4(builder)); + assert!(matches!(req.num_images, Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Four))); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/cost.rs new file mode 100644 index 0000000000..78f7b23eda --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/cost.rs @@ -0,0 +1,124 @@ +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::EnqueueBytedanceSeedreamV4EditImageNumImages; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::EnqueueBytedanceSeedreamV4TextToImageNumImages; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image_v2::providers::fal::seedream_4::request::FalSeedream4RequestState; + +pub struct FalSeedream4CostState { + num_images: u64, +} + +impl FalSeedream4CostState { + pub fn from_request(request: &FalSeedream4RequestState) -> Self { + let num_images = match request { + FalSeedream4RequestState::TextToImage(req) => match req.num_images { + Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::One) => 1, + Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two) => 2, + Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Three) => 3, + Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::Four) => 4, + None => 1, // default + }, + FalSeedream4RequestState::EditImage(req) => match req.num_images { + Some(EnqueueBytedanceSeedreamV4EditImageNumImages::One) => 1, + Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Two) => 2, + Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Three) => 3, + Some(EnqueueBytedanceSeedreamV4EditImageNumImages::Four) => 4, + None => 1, + }, + }; + Self { num_images } + } + + pub fn estimate_cost(&self) -> ImageGenerationCostEstimate { + // Pricing: $0.03 per image. Matches v1. + const COST_PER_IMAGE: u64 = 3; + 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_bytedance_seedream_v4_edit_image_webhook::EnqueueBytedanceSeedreamV4EditImageRequest; + use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::EnqueueBytedanceSeedreamV4TextToImageRequest; + + fn t2i_cost(n: EnqueueBytedanceSeedreamV4TextToImageNumImages) -> ImageGenerationCostEstimate { + FalSeedream4CostState::from_request(&FalSeedream4RequestState::TextToImage( + EnqueueBytedanceSeedreamV4TextToImageRequest { + prompt: "test".to_string(), + num_images: Some(n), + max_images: None, + image_size: None, + }, + )).estimate_cost() + } + + fn edit_cost(n: EnqueueBytedanceSeedreamV4EditImageNumImages) -> ImageGenerationCostEstimate { + FalSeedream4CostState::from_request(&FalSeedream4RequestState::EditImage( + EnqueueBytedanceSeedreamV4EditImageRequest { + prompt: "test".to_string(), + image_urls: vec!["https://example.com/x.jpg".to_string()], + num_images: Some(n), + max_images: None, + image_size: None, + }, + )).estimate_cost() + } + + #[test] + fn t2i_one_image_is_3_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4TextToImageNumImages::One).cost_in_usd_cents, Some(3)); + } + + #[test] + fn t2i_two_images_is_6_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two).cost_in_usd_cents, Some(6)); + } + + #[test] + fn t2i_three_images_is_9_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4TextToImageNumImages::Three).cost_in_usd_cents, Some(9)); + } + + #[test] + fn t2i_four_images_is_12_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4TextToImageNumImages::Four).cost_in_usd_cents, Some(12)); + } + + #[test] + fn t2i_none_num_images_defaults_to_one() { + let cost = FalSeedream4CostState::from_request(&FalSeedream4RequestState::TextToImage( + EnqueueBytedanceSeedreamV4TextToImageRequest { + prompt: "test".to_string(), + num_images: None, + max_images: None, + image_size: None, + }, + )).estimate_cost(); + assert_eq!(cost.cost_in_usd_cents, Some(3)); + } + + #[test] + fn edit_two_images_matches_t2i_two_images() { + assert_eq!( + edit_cost(EnqueueBytedanceSeedreamV4EditImageNumImages::Two).cost_in_usd_cents, + t2i_cost(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two).cost_in_usd_cents, + ); + } + + #[test] + fn credits_equal_cents() { + let c = t2i_cost(EnqueueBytedanceSeedreamV4TextToImageNumImages::Two); + assert_eq!(c.cost_in_credits, c.cost_in_usd_cents); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/request.rs new file mode 100644 index 0000000000..959cd5bebf --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4/request.rs @@ -0,0 +1,115 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{ + enqueue_bytedance_seedream_v4_edit_image_webhook, EnqueueBytedanceSeedreamV4EditImageArgs, + EnqueueBytedanceSeedreamV4EditImageRequest, +}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{ + enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs, + EnqueueBytedanceSeedreamV4TextToImageRequest, +}; + +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 FalSeedream4RequestState { + TextToImage(EnqueueBytedanceSeedreamV4TextToImageRequest), + EditImage(EnqueueBytedanceSeedreamV4EditImageRequest), +} + +impl FalSeedream4RequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound): (_, Arc) = match self { + Self::TextToImage(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueBytedanceSeedreamV4TextToImageArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_bytedance_seedream_v4_text_to_image_webhook(args).await, outbound) + } + Self::EditImage(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueBytedanceSeedreamV4EditImageArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_bytedance_seedream_v4_edit_image_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_bytedance_seedream_v4_edit_image_webhook::EnqueueBytedanceSeedreamV4EditImageNumImages; + use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::EnqueueBytedanceSeedreamV4TextToImageNumImages; + 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 = FalSeedream4RequestState::TextToImage(EnqueueBytedanceSeedreamV4TextToImageRequest { + prompt: "a corgi wearing sunglasses on a surfboard".to_string(), + num_images: Some(EnqueueBytedanceSeedreamV4TextToImageNumImages::One), + max_images: None, + image_size: None, + }); + let response = state.send(&client).await.expect("send should succeed"); + let payload = response.get_fal_payload().expect("expected Fal payload"); + println!("seedream_4 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 = FalSeedream4RequestState::EditImage(EnqueueBytedanceSeedreamV4EditImageRequest { + prompt: "make it sunset".to_string(), + image_urls: vec![JUNO_AT_LAKE_IMAGE_URL.to_string()], + num_images: Some(EnqueueBytedanceSeedreamV4EditImageNumImages::One), + max_images: None, + image_size: None, + }); + let response = state.send(&client).await.expect("send should succeed"); + let payload = response.get_fal_payload().expect("expected Fal payload"); + println!("seedream_4 edit — request_id: {:?}", payload.request_id); + assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/build.rs new file mode 100644 index 0000000000..57d6fc6513 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/build.rs @@ -0,0 +1,286 @@ +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{ + EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageRequest, + EnqueueBytedanceSeedreamV4p5EditImageSize, +}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{ + EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageRequest, + EnqueueBytedanceSeedreamV4p5TextToImageSize, +}; + +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::seedream_4p5::request::FalSeedream4p5RequestState; + +pub fn build_fal_seedream_4p5( + builder: GenerateImageRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + let image_urls = resolve_image_list_ref(builder.image_inputs.clone())?; + let image_size = plan_image_size(builder.aspect_ratio, strategy)?; + 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() { + FalSeedream4p5RequestState::TextToImage(EnqueueBytedanceSeedreamV4p5TextToImageRequest { + prompt, + num_images: Some(to_t2i_num_images(num_images)), + max_images: None, + image_size: image_size.map(to_t2i_image_size), + }) + } else { + FalSeedream4p5RequestState::EditImage(EnqueueBytedanceSeedreamV4p5EditImageRequest { + prompt, + image_urls, + num_images: Some(to_edit_num_images(num_images)), + max_images: None, + image_size: image_size.map(to_edit_image_size), + }) + }; + + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream4p5(state), + )) +} + +// ── Num images ── + +#[derive(Copy, Clone, Debug)] +enum PlannedNumImages { + One, + Two, + Three, + Four, +} + +fn plan_num_images( + count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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) -> EnqueueBytedanceSeedreamV4p5TextToImageNumImages { + use EnqueueBytedanceSeedreamV4p5TextToImageNumImages as T; + match n { + PlannedNumImages::One => T::One, + PlannedNumImages::Two => T::Two, + PlannedNumImages::Three => T::Three, + PlannedNumImages::Four => T::Four, + } +} + +fn to_edit_num_images(n: PlannedNumImages) -> EnqueueBytedanceSeedreamV4p5EditImageNumImages { + use EnqueueBytedanceSeedreamV4p5EditImageNumImages as E; + match n { + PlannedNumImages::One => E::One, + PlannedNumImages::Two => E::Two, + PlannedNumImages::Three => E::Three, + PlannedNumImages::Four => E::Four, + } +} + +// ── Image size ── +// +// Note: Seedream V4.5 lacks the bare "Auto" image_size; only Auto2k/Auto4k are supported. +// Any Auto request falls back to Auto2k. + +#[derive(Copy, Clone, Debug)] +enum PlannedImageSize { + Square, + SquareHd, + PortraitFourThree, + PortraitSixteenNine, + LandscapeFourThree, + LandscapeSixteenNine, + Auto2k, + Auto4k, +} + +fn plan_image_size( + aspect_ratio: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlannedImageSize as S; + match aspect_ratio { + None => Ok(None), + + // No bare Auto for v4.5 — fall back to Auto2k. + Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)), + Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)), + + 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(unsupported @ CommonAspectRatio::WideFiveByFour) + | Some(unsupported @ CommonAspectRatio::WideThreeByTwo) + | Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(S::LandscapeSixteenNine)), + }, + + Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)), + + Some(unsupported @ CommonAspectRatio::TallFourByFive) + | Some(unsupported @ CommonAspectRatio::TallTwoByThree) + | Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(S::PortraitSixteenNine)), + }, + } +} + +fn to_t2i_image_size(s: PlannedImageSize) -> EnqueueBytedanceSeedreamV4p5TextToImageSize { + use EnqueueBytedanceSeedreamV4p5TextToImageSize as T; + match s { + PlannedImageSize::Square => T::Square, + PlannedImageSize::SquareHd => T::SquareHd, + PlannedImageSize::PortraitFourThree => T::PortraitFourThree, + PlannedImageSize::PortraitSixteenNine => T::PortraitSixteenNine, + PlannedImageSize::LandscapeFourThree => T::LandscapeFourThree, + PlannedImageSize::LandscapeSixteenNine => T::LandscapeSixteenNine, + PlannedImageSize::Auto2k => T::Auto2k, + PlannedImageSize::Auto4k => T::Auto4k, + } +} + +fn to_edit_image_size(s: PlannedImageSize) -> EnqueueBytedanceSeedreamV4p5EditImageSize { + use EnqueueBytedanceSeedreamV4p5EditImageSize as E; + match s { + PlannedImageSize::Square => E::Square, + PlannedImageSize::SquareHd => E::SquareHd, + PlannedImageSize::PortraitFourThree => E::PortraitFourThree, + PlannedImageSize::PortraitSixteenNine => E::PortraitSixteenNine, + PlannedImageSize::LandscapeFourThree => E::LandscapeFourThree, + PlannedImageSize::LandscapeSixteenNine => E::LandscapeSixteenNine, + PlannedImageSize::Auto2k => E::Auto2k, + PlannedImageSize::Auto4k => E::Auto4k, + } +} + +// ── Image inputs ── + +fn resolve_image_list_ref( + image_list_ref: Option, +) -> Result, 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::Seedream4p5, + 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) -> EnqueueBytedanceSeedreamV4p5TextToImageRequest { + let ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream4p5(FalSeedream4p5RequestState::TextToImage(req)) + ) = result.expect("build should succeed") else { panic!("expected TextToImage variant") }; + req + } + + #[test] + fn auto_falls_back_to_auto2k() { + // Seedream 4.5 has no bare Auto — request with Auto should map to Auto2k. + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::Auto), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_4p5(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto2k))); + } + + #[test] + fn auto4k_stays_auto4k() { + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::Auto4k), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_4p5(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV4p5TextToImageSize::Auto4k))); + } + + #[test] + fn media_tokens_return_error() { + let builder = GenerateImageRequestBuilder { + image_inputs: Some(ImageListRef::MediaFileTokens(vec![])), + ..base_builder() + }; + assert!(matches!( + build_fal_seedream_4p5(builder), + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + )); + } + + #[test] + fn zero_batch_is_error() { + let builder = GenerateImageRequestBuilder { + image_batch_count: Some(0), + ..base_builder() + }; + assert!(matches!( + build_fal_seedream_4p5(builder), + Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)) + )); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/cost.rs new file mode 100644 index 0000000000..58cfc743e3 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/cost.rs @@ -0,0 +1,106 @@ +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::EnqueueBytedanceSeedreamV4p5EditImageNumImages; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::EnqueueBytedanceSeedreamV4p5TextToImageNumImages; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image_v2::providers::fal::seedream_4p5::request::FalSeedream4p5RequestState; + +pub struct FalSeedream4p5CostState { + num_images: u64, +} + +impl FalSeedream4p5CostState { + pub fn from_request(request: &FalSeedream4p5RequestState) -> Self { + let num_images = match request { + FalSeedream4p5RequestState::TextToImage(req) => match req.num_images { + Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One) => 1, + Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two) => 2, + Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Three) => 3, + Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four) => 4, + None => 1, + }, + FalSeedream4p5RequestState::EditImage(req) => match req.num_images { + Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::One) => 1, + Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two) => 2, + Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Three) => 3, + Some(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Four) => 4, + None => 1, + }, + }; + Self { num_images } + } + + pub fn estimate_cost(&self) -> ImageGenerationCostEstimate { + // Pricing: $0.04 per image. 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_bytedance_seedream_v4p5_edit_image_webhook::EnqueueBytedanceSeedreamV4p5EditImageRequest; + use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::EnqueueBytedanceSeedreamV4p5TextToImageRequest; + + fn t2i_cost(n: EnqueueBytedanceSeedreamV4p5TextToImageNumImages) -> ImageGenerationCostEstimate { + FalSeedream4p5CostState::from_request(&FalSeedream4p5RequestState::TextToImage( + EnqueueBytedanceSeedreamV4p5TextToImageRequest { + prompt: "test".to_string(), + num_images: Some(n), + max_images: None, + image_size: None, + }, + )).estimate_cost() + } + + fn edit_cost(n: EnqueueBytedanceSeedreamV4p5EditImageNumImages) -> ImageGenerationCostEstimate { + FalSeedream4p5CostState::from_request(&FalSeedream4p5RequestState::EditImage( + EnqueueBytedanceSeedreamV4p5EditImageRequest { + prompt: "test".to_string(), + image_urls: vec!["https://example.com/x.jpg".to_string()], + num_images: Some(n), + max_images: None, + image_size: None, + }, + )).estimate_cost() + } + + #[test] + fn t2i_one_image_is_4_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One).cost_in_usd_cents, Some(4)); + } + + #[test] + fn t2i_two_images_is_8_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two).cost_in_usd_cents, Some(8)); + } + + #[test] + fn t2i_four_images_is_16_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Four).cost_in_usd_cents, Some(16)); + } + + #[test] + fn edit_two_images_matches_t2i_two_images() { + assert_eq!( + edit_cost(EnqueueBytedanceSeedreamV4p5EditImageNumImages::Two).cost_in_usd_cents, + t2i_cost(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two).cost_in_usd_cents, + ); + } + + #[test] + fn credits_equal_cents() { + let c = t2i_cost(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::Two); + assert_eq!(c.cost_in_credits, c.cost_in_usd_cents); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/request.rs new file mode 100644 index 0000000000..b9c41d6abd --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_4p5/request.rs @@ -0,0 +1,96 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{ + enqueue_bytedance_seedream_v4p5_edit_image_webhook, EnqueueBytedanceSeedreamV4p5EditImageArgs, + EnqueueBytedanceSeedreamV4p5EditImageRequest, +}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{ + enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs, + EnqueueBytedanceSeedreamV4p5TextToImageRequest, +}; + +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 FalSeedream4p5RequestState { + TextToImage(EnqueueBytedanceSeedreamV4p5TextToImageRequest), + EditImage(EnqueueBytedanceSeedreamV4p5EditImageRequest), +} + +impl FalSeedream4p5RequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound): (_, Arc) = match self { + Self::TextToImage(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args).await, outbound) + } + Self::EditImage(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueBytedanceSeedreamV4p5EditImageArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_bytedance_seedream_v4p5_edit_image_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::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::EnqueueBytedanceSeedreamV4p5TextToImageNumImages; + + 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 = FalSeedream4p5RequestState::TextToImage(EnqueueBytedanceSeedreamV4p5TextToImageRequest { + prompt: "a corgi wearing sunglasses on a surfboard".to_string(), + num_images: Some(EnqueueBytedanceSeedreamV4p5TextToImageNumImages::One), + max_images: None, + image_size: None, + }); + let response = state.send(&client).await.expect("send should succeed"); + let payload = response.get_fal_payload().expect("expected Fal payload"); + println!("seedream_4p5 t2i — request_id: {:?}", payload.request_id); + assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/build.rs new file mode 100644 index 0000000000..838de45ece --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/build.rs @@ -0,0 +1,309 @@ +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{ + EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageRequest, + EnqueueBytedanceSeedreamV5LiteEditImageSize, +}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{ + EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageRequest, + EnqueueBytedanceSeedreamV5LiteTextToImageSize, +}; + +use crate::api::common_aspect_ratio::CommonAspectRatio; +use crate::api::common_resolution::CommonResolution; +use crate::api::image_list_ref::ImageListRef; +use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::errors::client_error::ClientError; +use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; +use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest; +use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest; +use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::request::FalSeedream5LiteRequestState; + +pub fn build_fal_seedream_5_lite( + builder: GenerateImageRequestBuilder, +) -> Result { + let strategy = builder.request_mismatch_mitigation_strategy; + let image_urls = resolve_image_list_ref(builder.image_inputs.clone())?; + let image_size = plan_image_size(builder.aspect_ratio, builder.resolution, strategy)?; + 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() { + FalSeedream5LiteRequestState::TextToImage(EnqueueBytedanceSeedreamV5LiteTextToImageRequest { + prompt, + num_images: Some(to_t2i_num_images(num_images)), + max_images: None, + image_size: image_size.map(to_t2i_image_size), + }) + } else { + FalSeedream5LiteRequestState::EditImage(EnqueueBytedanceSeedreamV5LiteEditImageRequest { + prompt, + image_urls, + num_images: Some(to_edit_num_images(num_images)), + max_images: None, + image_size: image_size.map(to_edit_image_size), + }) + }; + + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream5Lite(state), + )) +} + +// ── Num images ── + +#[derive(Copy, Clone, Debug)] +enum PlannedNumImages { + One, + Two, + Three, + Four, +} + +fn plan_num_images( + count: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result { + 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) -> EnqueueBytedanceSeedreamV5LiteTextToImageNumImages { + use EnqueueBytedanceSeedreamV5LiteTextToImageNumImages as T; + match n { + PlannedNumImages::One => T::One, + PlannedNumImages::Two => T::Two, + PlannedNumImages::Three => T::Three, + PlannedNumImages::Four => T::Four, + } +} + +fn to_edit_num_images(n: PlannedNumImages) -> EnqueueBytedanceSeedreamV5LiteEditImageNumImages { + use EnqueueBytedanceSeedreamV5LiteEditImageNumImages as E; + match n { + PlannedNumImages::One => E::One, + PlannedNumImages::Two => E::Two, + PlannedNumImages::Three => E::Three, + PlannedNumImages::Four => E::Four, + } +} + +// ── Image size ── +// +// Note: Seedream V5 Lite supports Auto2k and Auto3k (no 4K, no bare Auto). +// Auto4k requests fall back to Auto3k. + +#[derive(Copy, Clone, Debug)] +enum PlannedImageSize { + Square, + SquareHd, + PortraitFourThree, + PortraitSixteenNine, + LandscapeFourThree, + LandscapeSixteenNine, + Auto2k, + Auto3k, +} + +fn plan_image_size( + aspect_ratio: Option, + resolution: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + use PlannedImageSize as S; + match aspect_ratio { + // No aspect_ratio: use resolution as a hint for Auto sizes. + // Seedream v5 lite has no 4K — clamp to 3K. + None => match resolution { + Some(CommonResolution::TwoK) => Ok(Some(S::Auto2k)), + Some(CommonResolution::ThreeK) | Some(CommonResolution::FourK) => Ok(Some(S::Auto3k)), + _ => Ok(None), + }, + + Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)), + Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto3k)), + // No 4K — fall back to 3K. + Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)), + + 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(unsupported @ CommonAspectRatio::WideFiveByFour) + | Some(unsupported @ CommonAspectRatio::WideThreeByTwo) + | Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(S::LandscapeSixteenNine)), + }, + + Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)), + Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)), + + Some(unsupported @ CommonAspectRatio::TallFourByFive) + | Some(unsupported @ CommonAspectRatio::TallTwoByThree) + | Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "aspect_ratio", + value: format!("{:?}", unsupported), + })) + } + _ => Ok(Some(S::PortraitSixteenNine)), + }, + } +} + +fn to_t2i_image_size(s: PlannedImageSize) -> EnqueueBytedanceSeedreamV5LiteTextToImageSize { + use EnqueueBytedanceSeedreamV5LiteTextToImageSize as T; + match s { + PlannedImageSize::Square => T::Square, + PlannedImageSize::SquareHd => T::SquareHd, + PlannedImageSize::PortraitFourThree => T::PortraitFourThree, + PlannedImageSize::PortraitSixteenNine => T::PortraitSixteenNine, + PlannedImageSize::LandscapeFourThree => T::LandscapeFourThree, + PlannedImageSize::LandscapeSixteenNine => T::LandscapeSixteenNine, + PlannedImageSize::Auto2k => T::Auto2k, + PlannedImageSize::Auto3k => T::Auto3k, + } +} + +fn to_edit_image_size(s: PlannedImageSize) -> EnqueueBytedanceSeedreamV5LiteEditImageSize { + use EnqueueBytedanceSeedreamV5LiteEditImageSize as E; + match s { + PlannedImageSize::Square => E::Square, + PlannedImageSize::SquareHd => E::SquareHd, + PlannedImageSize::PortraitFourThree => E::PortraitFourThree, + PlannedImageSize::PortraitSixteenNine => E::PortraitSixteenNine, + PlannedImageSize::LandscapeFourThree => E::LandscapeFourThree, + PlannedImageSize::LandscapeSixteenNine => E::LandscapeSixteenNine, + PlannedImageSize::Auto2k => E::Auto2k, + PlannedImageSize::Auto3k => E::Auto3k, + } +} + +// ── Image inputs ── + +fn resolve_image_list_ref( + image_list_ref: Option, +) -> Result, 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::Seedream5Lite, + 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) -> EnqueueBytedanceSeedreamV5LiteTextToImageRequest { + let ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::FalSeedream5Lite(FalSeedream5LiteRequestState::TextToImage(req)) + ) = result.expect("build should succeed") else { panic!("expected TextToImage variant") }; + req + } + + #[test] + fn auto_maps_to_auto2k() { + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::Auto), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_5_lite(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto2k))); + } + + #[test] + fn auto4k_falls_back_to_auto3k() { + // Seedream 5 Lite has no 4K — clamp to 3K. + let builder = GenerateImageRequestBuilder { + aspect_ratio: Some(CommonAspectRatio::Auto4k), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_5_lite(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto3k))); + } + + #[test] + fn no_aspect_no_resolution_yields_none() { + let req = unwrap_t2i(build_fal_seedream_5_lite(base_builder())); + assert!(req.image_size.is_none()); + } + + #[test] + fn resolution_two_k_triggers_auto2k_when_no_aspect() { + let builder = GenerateImageRequestBuilder { + resolution: Some(CommonResolution::TwoK), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_5_lite(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto2k))); + } + + #[test] + fn resolution_four_k_clamps_to_auto3k() { + let builder = GenerateImageRequestBuilder { + resolution: Some(CommonResolution::FourK), + ..base_builder() + }; + let req = unwrap_t2i(build_fal_seedream_5_lite(builder)); + assert!(matches!(req.image_size, Some(EnqueueBytedanceSeedreamV5LiteTextToImageSize::Auto3k))); + } + + #[test] + fn media_tokens_return_error() { + let builder = GenerateImageRequestBuilder { + image_inputs: Some(ImageListRef::MediaFileTokens(vec![])), + ..base_builder() + }; + assert!(matches!( + build_fal_seedream_5_lite(builder), + Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls)) + )); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/cost.rs new file mode 100644 index 0000000000..9505b1b7fb --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/cost.rs @@ -0,0 +1,106 @@ +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::EnqueueBytedanceSeedreamV5LiteEditImageNumImages; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::EnqueueBytedanceSeedreamV5LiteTextToImageNumImages; + +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image_v2::providers::fal::seedream_5_lite::request::FalSeedream5LiteRequestState; + +pub struct FalSeedream5LiteCostState { + num_images: u64, +} + +impl FalSeedream5LiteCostState { + pub fn from_request(request: &FalSeedream5LiteRequestState) -> Self { + let num_images = match request { + FalSeedream5LiteRequestState::TextToImage(req) => match req.num_images { + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One) => 1, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two) => 2, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Three) => 3, + Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Four) => 4, + None => 1, + }, + FalSeedream5LiteRequestState::EditImage(req) => match req.num_images { + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::One) => 1, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two) => 2, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Three) => 3, + Some(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Four) => 4, + None => 1, + }, + }; + Self { num_images } + } + + pub fn estimate_cost(&self) -> ImageGenerationCostEstimate { + // Pricing: $0.035 per image — rounded up to 4 cents. 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_bytedance_seedream_v5_lite_edit_image_webhook::EnqueueBytedanceSeedreamV5LiteEditImageRequest; + use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::EnqueueBytedanceSeedreamV5LiteTextToImageRequest; + + fn t2i_cost(n: EnqueueBytedanceSeedreamV5LiteTextToImageNumImages) -> ImageGenerationCostEstimate { + FalSeedream5LiteCostState::from_request(&FalSeedream5LiteRequestState::TextToImage( + EnqueueBytedanceSeedreamV5LiteTextToImageRequest { + prompt: "test".to_string(), + num_images: Some(n), + max_images: None, + image_size: None, + }, + )).estimate_cost() + } + + fn edit_cost(n: EnqueueBytedanceSeedreamV5LiteEditImageNumImages) -> ImageGenerationCostEstimate { + FalSeedream5LiteCostState::from_request(&FalSeedream5LiteRequestState::EditImage( + EnqueueBytedanceSeedreamV5LiteEditImageRequest { + prompt: "test".to_string(), + image_urls: vec!["https://example.com/x.jpg".to_string()], + num_images: Some(n), + max_images: None, + image_size: None, + }, + )).estimate_cost() + } + + #[test] + fn t2i_one_image_is_4_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One).cost_in_usd_cents, Some(4)); + } + + #[test] + fn t2i_two_images_is_8_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two).cost_in_usd_cents, Some(8)); + } + + #[test] + fn t2i_four_images_is_16_cents() { + assert_eq!(t2i_cost(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Four).cost_in_usd_cents, Some(16)); + } + + #[test] + fn edit_two_images_matches_t2i_two_images() { + assert_eq!( + edit_cost(EnqueueBytedanceSeedreamV5LiteEditImageNumImages::Two).cost_in_usd_cents, + t2i_cost(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two).cost_in_usd_cents, + ); + } + + #[test] + fn credits_equal_cents() { + let c = t2i_cost(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::Two); + assert_eq!(c.cost_in_credits, c.cost_in_usd_cents); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/request.rs new file mode 100644 index 0000000000..1d9bf776af --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/fal/seedream_5_lite/request.rs @@ -0,0 +1,96 @@ +use std::fmt::Debug; +use std::sync::Arc; + +use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{ + enqueue_bytedance_seedream_v5_lite_edit_image_webhook, EnqueueBytedanceSeedreamV5LiteEditImageArgs, + EnqueueBytedanceSeedreamV5LiteEditImageRequest, +}; +use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{ + enqueue_bytedance_seedream_v5_lite_text_to_image_webhook, EnqueueBytedanceSeedreamV5LiteTextToImageArgs, + EnqueueBytedanceSeedreamV5LiteTextToImageRequest, +}; + +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 FalSeedream5LiteRequestState { + TextToImage(EnqueueBytedanceSeedreamV5LiteTextToImageRequest), + EditImage(EnqueueBytedanceSeedreamV5LiteEditImageRequest), +} + +impl FalSeedream5LiteRequestState { + pub async fn send(&self, client: &RouterFalClient) -> Result { + let (webhook_response, outbound): (_, Arc) = match self { + Self::TextToImage(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args).await, outbound) + } + Self::EditImage(request) => { + let outbound: Arc = Arc::new(request.clone()); + let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs { + request: request.clone(), + webhook_url: client.webhook_url.as_str(), + api_key: &client.api_key, + }; + (enqueue_bytedance_seedream_v5_lite_edit_image_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::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::EnqueueBytedanceSeedreamV5LiteTextToImageNumImages; + + 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 = FalSeedream5LiteRequestState::TextToImage(EnqueueBytedanceSeedreamV5LiteTextToImageRequest { + prompt: "a corgi wearing sunglasses on a surfboard".to_string(), + num_images: Some(EnqueueBytedanceSeedreamV5LiteTextToImageNumImages::One), + max_images: None, + image_size: None, + }); + let response = state.send(&client).await.expect("send should succeed"); + let payload = response.get_fal_payload().expect("expected Fal payload"); + println!("seedream_5_lite t2i — request_id: {:?}", payload.request_id); + assert!(payload.request_id.is_some() || payload.gateway_request_id.is_some()); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/mod.rs index bf6940e9bd..cded7c363d 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image_v2/providers/mod.rs @@ -1 +1,2 @@ +pub mod artcraft; pub mod fal; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/build_common.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/build_common.rs index 0bc2289065..cdfd086a90 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/build_common.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/build_common.rs @@ -37,7 +37,7 @@ pub enum UltraWideSupport { /// Build an `OmniGenVideoCostAndGenerateRequest` from the builder, using /// model-specific configuration for the model enum variant, resolution /// strategy, and ultra-wide aspect ratio support. -pub fn build_artcraft_omni_request( +pub fn build_artcraft_omni_video_request( mut builder: GenerateVideoRequestBuilder, model: CommonVideoModelEnum, resolutions: SupportedResolutions, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/build.rs index 57136530d2..b7ffbd4259 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::request::ArtcraftGrokImagineVideoRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -18,7 +18,7 @@ use crate::generate::generate_video_v2::video_generation_request::VideoGeneratio pub fn build_artcraft_grok_imagine_video( builder: GenerateVideoRequestBuilder, ) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::GrokImagineVideo, SupportedResolutions::Fast, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/request.rs index 35d7af95db..ae847b0eb7 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/grok_imagine_video/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftGrokImagineVideoRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftGrokImagineVideoRequestState { impl ArtcraftGrokImagineVideoRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/build.rs index 4dec7ed7ad..0409013935 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_happy_horse_1p0(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::HappyHorse1p0, SupportedResolutions::NoFourEightyP, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/request.rs index 36ca4ed99b..465b8c9272 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/happy_horse_1p0/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftHappyHorse1p0RequestState { @@ -12,7 +12,7 @@ pub struct ArtcraftHappyHorse1p0RequestState { impl ArtcraftHappyHorse1p0RequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/build.rs index 36194fbf1c..8f98f417a4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_1_6_pro::request::ArtcraftKling16ProRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_kling_1_6_pro(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling16Pro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/request.rs index 3668d71732..4c5f3dbff4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_1_6_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling16ProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling16ProRequestState { impl ArtcraftKling16ProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/build.rs index 8f19343a78..2c10be955b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_2_1_master::request::ArtcraftKling21MasterRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_kling_2_1_master(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling21Master, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/request.rs index 3abb006fb5..fe3c9e25e8 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_master/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling21MasterRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling21MasterRequestState { impl ArtcraftKling21MasterRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/build.rs index 5987b7e835..e48ab42310 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_2_1_pro::request::ArtcraftKling21ProRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_kling_2_1_pro(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling21Pro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/request.rs index 69ea6aea0a..55df27f90e 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_1_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling21ProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling21ProRequestState { impl ArtcraftKling21ProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/build.rs index 36a98df551..cdd0d47ecb 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_2_5_turbo_pro::request::ArtcraftKling2p5TurboProRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_kling_2_5_turbo_pro(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling2p5TurboPro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/request.rs index 1cac9144dc..0ce0c8d748 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_5_turbo_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling2p5TurboProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling2p5TurboProRequestState { impl ArtcraftKling2p5TurboProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/build.rs index 18e97fe42e..c929e88ccd 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_2_6_pro::request::ArtcraftKling2p6ProRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -14,7 +14,7 @@ pub fn build_artcraft_kling_2_6_pro(builder: GenerateVideoRequestBuilder) -> Res // build_artcraft_omni_request hardcodes generate_audio = None on its output, // so we extract before, build, then restore. let generate_audio = builder.generate_audio; - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling2p6Pro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/request.rs index 2dadb283b7..94e65406b3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_2_6_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling2p6ProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling2p6ProRequestState { impl ArtcraftKling2p6ProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/build.rs index 37cb59b727..bb8c13e0fd 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/build.rs @@ -5,7 +5,7 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_3p0_pro::request::ArtcraftKling3p0ProRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -21,7 +21,7 @@ pub fn build_artcraft_kling_3p0_pro(mut builder: GenerateVideoRequestBuilder) -> let final_duration = plan_kling_3p0_duration(builder.duration_seconds, strategy)?; builder.duration_seconds = final_duration.map(|d| d.max(4)); - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling3p0Pro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/request.rs index 735dba9be7..30bb939f16 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling3p0ProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling3p0ProRequestState { impl ArtcraftKling3p0ProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/build.rs index a34eb196bd..c2be433a9c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::kling_3p0_pro::build::plan_kling_3p0_duration; use crate::generate::generate_video_v2::providers::artcraft::kling_3p0_standard::request::ArtcraftKling3p0StandardRequestState; @@ -17,7 +17,7 @@ pub fn build_artcraft_kling_3p0_standard(mut builder: GenerateVideoRequestBuilde let final_duration = plan_kling_3p0_duration(builder.duration_seconds, strategy)?; builder.duration_seconds = final_duration.map(|d| d.max(4)); - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling3p0Standard, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/request.rs index 2f435b6f61..698c099ba9 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/kling_3p0_standard/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftKling3p0StandardRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftKling3p0StandardRequestState { impl ArtcraftKling3p0StandardRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs index 856a25cb51..b3014bf0a7 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::preview_model::request::ArtcraftPreviewModelRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_preview_model(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::PreviewModel, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs index 695d3cb9c8..4fbf0b8788 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftPreviewModelRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftPreviewModelRequestState { impl ArtcraftPreviewModelRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs index dfcfc28f75..78c7fdab85 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::request::ArtcraftPreviewModelFastRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_preview_model_fast(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::PreviewModelFast, SupportedResolutions::Fast, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs index bc69b66f0f..6941cafc54 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/preview_model_fast/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftPreviewModelFastRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftPreviewModelFastRequestState { impl ArtcraftPreviewModelFastRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/request_common.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/request_common.rs index 6d3bd5482d..d00a4cdd19 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/request_common.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/request_common.rs @@ -10,7 +10,7 @@ use crate::generate::generate_video::generate_video_response::{ /// Send a video generation request to the Artcraft omni-gen endpoint. /// All Artcraft model request states delegate to this function. -pub async fn send_artcraft_omni_request( +pub async fn send_artcraft_omni_video_request( request: &OmniGenVideoCostAndGenerateRequest, client: &RouterArtcraftClient, ) -> Result { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/build.rs index 94f38f4f6c..85f7fc9ccc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/build.rs @@ -5,7 +5,7 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_1p0_lite::request::ArtcraftSeedance10LiteRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -17,7 +17,7 @@ pub fn build_artcraft_seedance_1p0_lite(mut builder: GenerateVideoRequestBuilder let final_duration = plan_seedance_1p0_lite_duration(builder.duration_seconds, strategy)?; builder.duration_seconds = final_duration; - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance10Lite, SupportedResolutions::Fast, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/request.rs index 80ec47d992..23c968156f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p0_lite/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance10LiteRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftSeedance10LiteRequestState { impl ArtcraftSeedance10LiteRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/build.rs index 32303dd674..7f51073c7c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/build.rs @@ -5,7 +5,7 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_1p5_pro::request::ArtcraftSeedance1p5ProRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -20,7 +20,7 @@ pub fn build_artcraft_seedance_1p5_pro(mut builder: GenerateVideoRequestBuilder) builder.duration_seconds = final_duration; let generate_audio = builder.generate_audio; - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance1p5Pro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/request.rs index b5a674e747..0e78853fcc 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_1p5_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance1p5ProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftSeedance1p5ProRequestState { impl ArtcraftSeedance1p5ProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs index 001badfbb8..4425d2ab8c 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0::request::ArtcraftSeedance2p0RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_seedance_2p0(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance2p0, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs index ef7f7dc285..5346e8b6e3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0RequestState { @@ -12,7 +12,7 @@ pub struct ArtcraftSeedance2p0RequestState { impl ArtcraftSeedance2p0RequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/build.rs index c644b1b033..a6a70875bd 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp::request::ArtcraftSeedance2p0BytePlusRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_seedance_2p0_bp(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance2p0BytePlus, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/request.rs index d4ff2cf083..691f16c851 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0BytePlusRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftSeedance2p0BytePlusRequestState { impl ArtcraftSeedance2p0BytePlusRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/build.rs index 0bec0ba4f9..d83303c82e 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_bp_fast::request::ArtcraftSeedance2p0BytePlusFastRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_seedance_2p0_bp_fast(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance2p0BytePlusFast, SupportedResolutions::Fast, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/request.rs index 6d0a7d382f..39a4180b4b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_bp_fast/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0BytePlusFastRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftSeedance2p0BytePlusFastRequestState { impl ArtcraftSeedance2p0BytePlusFastRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs index 4008014382..38223260d3 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_fast::request::ArtcraftSeedance2p0FastRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_seedance_2p0_fast(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance2p0Fast, SupportedResolutions::Fast, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/request.rs index 058bc6a65a..43bee43220 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_fast/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0FastRequestState { @@ -12,7 +12,7 @@ pub struct ArtcraftSeedance2p0FastRequestState { impl ArtcraftSeedance2p0FastRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/build.rs index c4ed48c311..6b969e2fe2 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u::request::ArtcraftSeedance2p0UltraRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_seedance_2p0_u(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance2p0Ultra, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/request.rs index 82184c11ef..0a72a0e562 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0UltraRequestState { @@ -12,7 +12,7 @@ pub struct ArtcraftSeedance2p0UltraRequestState { impl ArtcraftSeedance2p0UltraRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/build.rs index 7e9454e5bf..6ce5b2f092 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/build.rs @@ -3,14 +3,14 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::seedance_2p0_u_fast::request::ArtcraftSeedance2p0UltraFastRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest; pub fn build_artcraft_seedance_2p0_u_fast(builder: GenerateVideoRequestBuilder) -> Result { - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance2p0UltraFast, SupportedResolutions::Fast, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/request.rs index fd1e433553..404c625dbd 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/seedance_2p0_u_fast/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSeedance2p0UltraFastRequestState { @@ -12,7 +12,7 @@ pub struct ArtcraftSeedance2p0UltraFastRequestState { impl ArtcraftSeedance2p0UltraFastRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/build.rs index 1e76a589c5..3096e27542 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/build.rs @@ -5,7 +5,7 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::sora_2::request::ArtcraftSora2RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -18,7 +18,7 @@ pub fn build_artcraft_sora_2(mut builder: GenerateVideoRequestBuilder) -> Result let final_duration = plan_sora_2_duration(builder.duration_seconds, strategy)?; builder.duration_seconds = final_duration; - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Sora2, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/request.rs index c6dd6afa4d..e58fd06d79 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSora2RequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftSora2RequestState { impl ArtcraftSora2RequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/build.rs index 5e960271c4..aa619dc9d7 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::sora_2::build::plan_sora_2_duration; use crate::generate::generate_video_v2::providers::artcraft::sora_2_pro::request::ArtcraftSora2ProRequestState; @@ -16,7 +16,7 @@ pub fn build_artcraft_sora_2_pro(mut builder: GenerateVideoRequestBuilder) -> Re let final_duration = plan_sora_2_duration(builder.duration_seconds, strategy)?; builder.duration_seconds = final_duration; - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Sora2Pro, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/request.rs index 587812f18a..351adce25a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/sora_2_pro/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftSora2ProRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftSora2ProRequestState { impl ArtcraftSora2ProRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/build.rs index 310d372dda..02cb6c0b2f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/build.rs @@ -5,7 +5,7 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::veo_2::request::ArtcraftVeo2RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -19,7 +19,7 @@ pub fn build_artcraft_veo_2(mut builder: GenerateVideoRequestBuilder) -> Result< let final_duration = plan_veo_2_duration(builder.duration_seconds, strategy)?; builder.duration_seconds = final_duration; - let request = build_artcraft_omni_request( + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Veo2, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/request.rs index 719e416717..5077291052 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_2/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftVeo2RequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftVeo2RequestState { impl ArtcraftVeo2RequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3/build.rs index 82845d3a41..8de5dced68 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::veo_3::request::ArtcraftVeo3RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -15,7 +15,7 @@ pub fn build_artcraft_veo_3(builder: GenerateVideoRequestBuilder) -> Result Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/build.rs index aaefce6934..8bf5bdaf99 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::veo_3_fast::request::ArtcraftVeo3FastRequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -14,7 +14,7 @@ pub fn build_artcraft_veo_3_fast(builder: GenerateVideoRequestBuilder) -> Result // generate_audio, and rely on cost.rs to bill 8s regardless of the request's // stored duration. let generate_audio = builder.generate_audio; - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Veo3Fast, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/request.rs index 2c5a3990e8..1f4103c8c7 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3_fast/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftVeo3FastRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftVeo3FastRequestState { impl ArtcraftVeo3FastRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/build.rs index 5d814fc6da..d5d4c1227b 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/build.rs @@ -5,7 +5,7 @@ use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::errors::client_error::ClientError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::veo_3p1::request::ArtcraftVeo3p1RequestState; use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest; @@ -20,7 +20,7 @@ pub fn build_artcraft_veo_3p1(mut builder: GenerateVideoRequestBuilder) -> Resul builder.duration_seconds = final_duration; let generate_audio = builder.generate_audio; - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Veo3p1, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/request.rs index 6b4ff1e667..cf0ddd987f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftVeo3p1RequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftVeo3p1RequestState { impl ArtcraftVeo3p1RequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/build.rs index 148b0b7266..c156835193 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/build.rs @@ -3,7 +3,7 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVid use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video_v2::providers::artcraft::build_common::{ - build_artcraft_omni_request, SupportedResolutions, UltraWideSupport, + build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, }; use crate::generate::generate_video_v2::providers::artcraft::veo_3p1::build::plan_veo_3p1_duration; use crate::generate::generate_video_v2::providers::artcraft::veo_3p1_fast::request::ArtcraftVeo3p1FastRequestState; @@ -17,7 +17,7 @@ pub fn build_artcraft_veo_3p1_fast(mut builder: GenerateVideoRequestBuilder) -> builder.duration_seconds = final_duration; let generate_audio = builder.generate_audio; - let mut request = build_artcraft_omni_request( + let mut request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Veo3p1Fast, SupportedResolutions::Full, diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/request.rs index 70597588d2..4d78272115 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video_v2/providers/artcraft/veo_3p1_fast/request.rs @@ -3,7 +3,7 @@ use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost use crate::client::router_artcraft_client::RouterArtcraftClient; use crate::errors::artcraft_router_error::ArtcraftRouterError; use crate::generate::generate_video::generate_video_response::GenerateVideoResponse; -use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request; +use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request; #[derive(Clone, Debug)] pub struct ArtcraftVeo3p1FastRequestState { @@ -12,6 +12,6 @@ pub struct ArtcraftVeo3p1FastRequestState { impl ArtcraftVeo3p1FastRequestState { pub async fn send(&self, client: &RouterArtcraftClient) -> Result { - send_artcraft_omni_request(&self.request, client).await + send_artcraft_omni_video_request(&self.request, client).await } } diff --git a/crates/schema/public/enums/src/common/generation/common_image_model.rs b/crates/schema/public/enums/src/common/generation/common_image_model.rs index 1c756fe96c..d7dea22c08 100644 --- a/crates/schema/public/enums/src/common/generation/common_image_model.rs +++ b/crates/schema/public/enums/src/common/generation/common_image_model.rs @@ -41,6 +41,10 @@ pub enum CommonImageModel { Seedream4p5, #[serde(rename = "seedream_5_lite")] Seedream5Lite, + #[serde(rename = "qwen_edit_2511_angles")] + QwenEdit2511Angles, + #[serde(rename = "flux_2_lora_angles")] + Flux2LoraAngles, } impl CommonImageModel { @@ -62,6 +66,8 @@ impl CommonImageModel { Self::Seedream4 => CommonModelType::Seedream4, Self::Seedream4p5 => CommonModelType::Seedream4p5, Self::Seedream5Lite => CommonModelType::Seedream5Lite, + Self::QwenEdit2511Angles => CommonModelType::QwenEdit2511Angles, + Self::Flux2LoraAngles => CommonModelType::Flux2LoraAngles, } } } @@ -93,6 +99,8 @@ mod tests { assert_serialization(CommonImageModel::Seedream4, "seedream_4"); assert_serialization(CommonImageModel::Seedream4p5, "seedream_4p5"); assert_serialization(CommonImageModel::Seedream5Lite, "seedream_5_lite"); + assert_serialization(CommonImageModel::QwenEdit2511Angles, "qwen_edit_2511_angles"); + assert_serialization(CommonImageModel::Flux2LoraAngles, "flux_2_lora_angles"); } #[test] @@ -113,6 +121,8 @@ mod tests { (CommonImageModel::Seedream4, CommonModelType::Seedream4), (CommonImageModel::Seedream4p5, CommonModelType::Seedream4p5), (CommonImageModel::Seedream5Lite, CommonModelType::Seedream5Lite), + (CommonImageModel::QwenEdit2511Angles, CommonModelType::QwenEdit2511Angles), + (CommonImageModel::Flux2LoraAngles, CommonModelType::Flux2LoraAngles), ]; for (image_model, expected) in models { assert_eq!(image_model.to_common_model_type(), expected); @@ -139,7 +149,7 @@ mod tests { #[test] fn variant_count() { use strum::IntoEnumIterator; - assert_eq!(CommonImageModel::iter().len(), 15); + assert_eq!(CommonImageModel::iter().len(), 17); } #[test] diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs index 7bf5d9071c..baf70e7cf4 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/cost_estimate/image/estimate_image_cost_handler.rs @@ -144,6 +144,8 @@ fn map_image_model(model: CommonImageModel) -> Result RouterImageModel::Seedream5Lite, CommonImageModel::GrokImagineImage => RouterImageModel::GrokImagineImage, CommonImageModel::GrokImagineImageQuality => RouterImageModel::GrokImagineImageQuality, + CommonImageModel::QwenEdit2511Angles => RouterImageModel::QwenEdit2511Angles, + CommonImageModel::Flux2LoraAngles => RouterImageModel::Flux2LoraAngles, }; Ok(router_model) }