Compare commits

...
Author SHA1 Message Date
Brandon Thomas 38cbd724d4 codegen + fixes 2026-06-03 10:36:58 -04:00
Brandon Thomas f669e0071e list email address changes 2026-06-03 01:08:54 -04:00
Brandon Thomas a8af0e8141 mod endpoint to update email 2026-06-03 00:51:30 -04:00
Brandon Thomas a78ac8c391 cleanup 2026-06-03 00:38:02 -04:00
Brandon Thomas 3a62afb53c usernames 2026-06-03 00:33:52 -04:00
Brandon Thomas 998a7b60b1 queries for new table 2026-06-03 00:30:02 -04:00
Brandon Thomas aeebad3a40 enum variant 2026-06-03 00:21:17 -04:00
Brandon Thomas bb53238c56 new table to track email changes 2026-06-03 00:10:13 -04:00
Brandon Thomas ca43d5cbe8 grok failure update (#1603) 2026-06-02 22:00:10 -04:00
BFlat 1d8f64dd23 webapp: midjourney icon and name mappings (#1602) 2026-06-02 22:58:33 +07:00
BFlat 2a49226479 mobile view update (#1601)
- Separate mobile generations page
- Bottom mobile navbar
2026-06-02 20:36:02 +07:00
Brandon Thomas a6f8d2a4aa midjourney images - router, web, jobs, etc. (#1600) 2026-06-02 06:49:12 -04:00
Brandon Thomas 017b923c5a midjourney client (#1599) 2026-06-02 04:02:09 -04:00
Brandon Thomas f0244c6da8 analytics queries (#1598) 2026-06-01 23:55:52 -04:00
Brandon Thomas 75788d9525 kling 1.6 pro, 2.5 turbo pro, 2.6 pro -> web omni api / artcraft_router / fal_client (#1597) 2026-06-01 19:08:25 -04:00
BFlat c5dcf0e155 app styling updates (#1596)
darker tone
2026-06-01 18:17:14 +07:00
Brandon Thomas c71af2b2b0 move deprecated modules (the bulk of them) (#1595) 2026-05-31 23:59:25 -04:00
Brandon Thomas 2f808fe61e artcraft_router cleanup from old migration (#1594) 2026-05-31 23:42:50 -04:00
Brandon Thomas 0c1bb0794a auto aspect ratio for grok 2026-05-31 16:39:46 -04:00
Brandon Thomas f551a547a0 grok max length is 4096 2026-05-31 15:05:09 -04:00
Brandon Thomas 8ac4e534e1 refactor to make intention of request prereq checks clear + reusable 2026-05-31 14:40:10 -04:00
BFlat e39fe7c106 textToVideoSupported api field (#1593) 2026-06-01 01:30:24 +07:00
Brandon Thomas e3d30a677a 400 on grok ttv (#1592) 2026-05-31 14:19:13 -04:00
Brandon Thomas e8a2cbd490 text_to_video_supported=false on new grok model (#1591) 2026-05-31 13:53:02 -04:00
Brandon Thomas 0d7168c506 grok imagine video 1.5 preview (#1590) 2026-05-31 12:21:54 -04:00
bflatastic 5d4f307320 model selector width update 2026-05-30 01:03:13 +07:00
Brandon Thomas dbd723541f add prompt token to GET /v1/media_files/list/user/{username} (#1589) 2026-05-29 13:45:17 -04:00
bflatastic c589f20110 popover updates for info tooltip 2026-05-30 00:42:55 +07:00
Brandon Thomas 59da9a2a79 descriptions got switched 2026-05-29 13:38:03 -04:00
BFlat 58d4f4ff12 webapp: extra info for models (#1588) 2026-05-30 00:30:14 +07:00
Brandon Thomas 10cb0b3030 fix build 2026-05-29 13:09:46 -04:00
Brandon Thomas 55d375895c description fields on video models 2026-05-29 13:05:55 -04:00
Brandon Thomas 5dfa059523 disable characters 2026-05-28 19:46:46 -04:00
bflatastic 9598a86dcd webapp: recreate failed jobs 2026-05-29 06:40:54 +07:00
Brandon Thomas d86da55954 update model list (#1587) 2026-05-28 19:10:36 -04:00
Brandon Thomas 6f9e240b4e ultra variant backend (#1586) 2026-05-28 19:03:12 -04:00
Brandon Thomas 491715e5a6 seedance variant - BytePlus "Ultra" (#1585) 2026-05-28 16:23:56 -04:00
Brandon Thomas 6e55dc63c0 group providers together (#1584) 2026-05-28 15:14:24 -04:00
Brandon Thomas 7aa8d9b7ca enums renames for less confusion (#1583) 2026-05-27 22:51:42 -04:00
Brandon Thomas 094afb4cf0 remove legacy image pipeline (#1582) 2026-05-27 18:30:54 -04:00
bflatastic cfd60a4d11 webapp: create row card styling fixes and copy prompt on failed and in progress rows 2026-05-27 14:30:29 +07:00
BFlat 2b0a692634 webapp: create pages list view (#1581)
Create pages now have a toggle for grid/list view depending on
preference
2026-05-27 14:14:36 +07:00
Brandon Thomas 46b3c92c8b artcraft router for images - pipeline v2 (#1580) 2026-05-26 18:10:30 -04:00
bflatastic 2b0c5b9085 webapp: revert previous google flow fix - check subscription instead 2026-05-26 23:39:04 +07:00
BFlat 006068a4c5 webapp: route new Google SSO users to the welcome page (once) (#1579)
After Google sign-in, brand-new accounts now land on /welcome instead of
the set-password page; returning users go to their normal destination.

"New user" is detected via the SSO response's
username_not_yet_customized (forced to false by the backend for
returning Google logins, so repeat logins never re-trigger it). A
per-user-token localStorage marker guards against the rare
email→Google-link edge case so welcome shows at most once per account.

- session.ts: add consumeNewUserWelcome(isNewUser) helper
- login / signup-form / signup-cta-modal: route via the helper
2026-05-26 23:02:44 +07:00
BFlat ee4a7f7e2f webapp: flow update password setting completely optional (#1578) 2026-05-26 22:28:27 +07:00
bflatastic 5d100a60e9 website: landing font fix 2026-05-26 21:56:09 +07:00
Brandon Thomas 370d2fc494 artcraft router: all video is on "v2"; artcraft desktop: all models on "omni"; storyteller-web: omni on video v2 (#1577)
This is a lot of legacy path cleanup. 

These are now the only valid paths:

- artcraft website: storyteller-web omni video gen -> artcraft_router v2
video pipeline

- artcraft desktop: artcraft_router v2 video pipeline (for artcraft
provider) -> storyteller-web omni video gen
2026-05-26 10:25:59 -04:00
BFlat c36c0d283c webapp: mobile responsive settings modal (#1576)
- settings modal is now full height and width on mobile with side tabs
laid horizontally at the top
2026-05-26 20:51:57 +07:00
BFlat a8f464e7ec webapp: only render background decoration on desktop (not mobile) for performance 2026-05-26 20:35:21 +07:00
BFlat 2d6cb2f03c fix test 4: revert glass blue styling 2026-05-26 20:20:21 +07:00
BFlat b82029484a fix test 3: mobile freeze 2026-05-26 20:05:21 +07:00
BFlat 73171cd3a8 fix: stop mobile freeze when overlays open over the prompt box
On mobile Safari, opening any overlay (mobile menu, signup modal) over the
create pages froze the UI for 5-10s. Cause: the fixed prompt box uses the
`.glass` class (`backdrop-filter: blur`), and iOS Safari re-evaluates a
backdrop-filter against the newly composited overlay content, hanging the
main thread. Desktop was unaffected.

- Disable every `backdrop-filter` on mobile (<= 639px) via a universal reset.
- Make `.glass` fully opaque on mobile so dropping the blur is invisible.
- Revert the earlier Google Sign-In deferral attempt in GoogleLoginButton
  (it didn't address the real cause).
2026-05-26 19:55:08 +07:00
BFlat 33be9f1ca4 ## Fix: mobile signup modal freeze (defer Google Sign-In)
**Problem:** On mobile Safari, tapping "Sign up to create" on the create pages froze for 5–10s before the signup modal appeared. Desktop was fine.

**Cause:** The modal mounts Google's GSI sign-in widget (`<GoogleLogin>`), which boots a cross-site iframe to `accounts.google.com`. Mobile Safari's tracking prevention (ITP) throttles that cross-site init for several seconds, and because the widget mounted synchronously when the modal opened, the modal's first paint was blocked behind it. The login/signup pages didn't show this because they mount the widget during a route change, hiding the cost.

**Fix:** Defer mounting the real GSI iframe until after first paint (`useEffect` + `requestAnimationFrame`). The custom "Continue with Google" button shows instantly; the actual Google button hydrates a frame later, off the critical path. The container has a fixed height, so there's no layout shift.

- Centralized in `GoogleLoginButton`, so the login/signup pages benefit too.
- Tradeoff: the Google button becomes clickable ~1 frame later (imperceptible).
- No behavior change on desktop.
2026-05-26 19:34:36 +07:00
Brandon Thomas 33116e0e5b artcraft router: video pipeline v2 implementations (#1575) 2026-05-26 06:54:29 -04:00
BFlat ebe2129b4b webapp: UX, CTA improvements (#1574)
## Signup, credits, and pricing polish

A round of UX improvements around signing up, running out of credits,
and the pricing page.

**Signup**
- Refreshed the signup prompt copy and added a "Continue with Google"
option (same flow as the login/signup pages)
- Added a "Sign up to create" button on the image and video pages for
logged-out visitors

**Create pages**
- Punchier empty-state headlines ("Describe anything. See it in
seconds." for images, "Describe a scene. See it in motion." for video)
- Added a subtle background pattern behind the empty state

**Out of credits**
- New modal that appears when a generation is blocked by not enough
credits
- Subscribers see "Upgrade plan" (hidden if they're already on the top
plan) plus "Buy credits"
- People without a plan get a "Choose a plan" prompt instead

**Pricing page**
- Existing subscribers get a "Buy more credits" shortcut at the top of
the page
- "Contact Us" now opens a small menu to copy the email or open your
mail app

**Cleanup**
- Removed the "Seedance 2.0 is in Early Alpha" warning from the task
queue

**Buy credits modal**
- Restyled the buy credits modal to be more appealing
2026-05-26 16:08:50 +07:00
Brandon Thomas 94077a3ecb test fix 2026-05-26 04:23:47 -04:00
Brandon Thomas 34c6af3145 test fix (#1573) 2026-05-26 04:22:57 -04:00
Brandon Thomas a7b9ec1d95 consolidate more error types (#1572) 2026-05-25 22:55:29 -04:00
Brandon Thomas 8ecc8cf7b3 clean up and consolidate bespoke error types (#1571) 2026-05-25 21:54:32 -04:00
Brandon Thomas 03a008fb5c use CommonWebError for alert visibility during google SSO (#1570) 2026-05-25 20:21:12 -04:00
Brandon Thomas 2c2594c482 custom serialize error type (#1569) 2026-05-25 18:49:05 -04:00
1252 changed files with 42432 additions and 38107 deletions
Generated
+2 -1
View File
@@ -805,11 +805,11 @@ dependencies = [
"gmicloud_client",
"grok_api_client",
"log",
"muapi_client",
"reqwest 0.12.23",
"seedance2pro_client",
"serde",
"serde_derive",
"serde_json",
"strum",
"test_data",
"tokens",
@@ -11542,6 +11542,7 @@ dependencies = [
"elasticsearch",
"elasticsearch_schema",
"enums",
"erased-serde",
"error_utils",
"errors",
"fal_client",
+1
View File
@@ -334,6 +334,7 @@ regex = "1.12.3"
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls", "gzip" ] }
reqwest-websocket = { version = "0.5.0" }
rust-s3 = { version = "0.36.0-beta.2", default-features = false, features = ["fail-on-err", "tags", "tokio-rustls-tls"] }
erased-serde = "0.4"
serde = { version = "1.0.228", features = ["derive"] }
serde_derive = "1.0.228"
serde_json = "1.0.149"
@@ -0,0 +1,23 @@
-- Daily ArtCraft generations broken down by model_type since January 1st, 2026.
-- One row per (day, model_type) pair.
SELECT
DATE(created_at) AS day,
maybe_model_type AS model_type,
COUNT(*) AS generations
FROM generic_inference_jobs
WHERE created_at >= '2026-01-01'
AND maybe_model_type IS NOT NULL
AND job_type NOT IN (
'gpt_sovits',
'tacotron2',
'rvc_v2',
'f5_tts',
'so_vits_svc',
'styletts2',
'face_fusion',
'seed_vc',
'comfy_ui'
)
GROUP BY day, model_type
ORDER BY day DESC, generations DESC;
@@ -0,0 +1,53 @@
select
u.created_at,
CASE
WHEN u.maybe_landing_url LIKE '%?r=%' THEN 'user_referral'
WHEN u.maybe_landing_url LIKE '%gad_source%' THEN 'google_ads'
WHEN u.maybe_landing_url LIKE '%utm_source=fakeyou%' THEN 'fakeyou'
WHEN u.maybe_landing_url LIKE '%utm_source=storyteller%' THEN 'storyteller'
WHEN u.maybe_landing_url LIKE '%utm_source=artcraftai%' THEN 'artcraft_dot_ai_domain'
WHEN u.maybe_referral_url LIKE '%instagram%' THEN 'instagram'
WHEN u.maybe_referral_url LIKE '%reddit%' THEN 'reddit'
WHEN u.maybe_referral_url LIKE '%google%' THEN 'google'
WHEN u.maybe_referral_url LIKE '%brave%' THEN 'brave'
WHEN u.maybe_referral_url LIKE '%duckduckgo%' THEN 'duckduckgo'
WHEN u.maybe_referral_url LIKE '%yahoo.com%' THEN 'yahoo'
WHEN u.maybe_referral_url LIKE '%yandex%' THEN 'yandex'
WHEN u.maybe_referral_url LIKE '%bing.com%' THEN 'bing'
WHEN u.maybe_referral_url LIKE '%github.com%' THEN 'github'
WHEN u.maybe_referral_url LIKE '%chatgpt%' THEN 'chatgpt'
WHEN u.maybe_referral_url LIKE '%https://t.co%' THEN 'twitter'
WHEN u.maybe_referral_url LIKE '%x.com%' THEN 'twitter'
WHEN u.maybe_referral_url LIKE '%youtube%' THEN 'youtube'
WHEN u.maybe_referral_url LIKE '%fakeyou.com%' THEN 'fakeyou'
WHEN u.maybe_referral_url LIKE '%ycombinator%' THEN 'hackernews'
WHEN u.maybe_referral_url LIKE '%seedanceprice%' THEN 'seedanceprice_reddit'
WHEN u.maybe_landing_url LIKE '%utm_source=ig%' THEN 'instagram'
WHEN u.maybe_landing_url LIKE '%utm_source=chatgpt%' THEN 'chatgpt'
WHEN u.maybe_landing_url LIKE '%/media/%' THEN 'reddit'
ELSE 'unknown'
END AS user_source,
u.maybe_referral_url,
u.maybe_landing_url,
u.username,
us.maybe_stripe_customer_id as subscription_customer,
l.stripe_customer_id as credits_customer,
u.email_address,
CASE
WHEN u.maybe_source LIKE '%artcraft_get_web%' THEN 'website'
WHEN u.maybe_source LIKE '%artcraft%' THEN 'app_or_website'
WHEN u.maybe_source LIKE '%artcraft_get_s%' THEN 'website_stripe_checkout'
ELSE u.maybe_source
END AS maybe_source,
u.maybe_signup_method,
u.created_at,
u.updated_at
from users u
left outer join user_subscriptions us
on us.user_token = u.token
and us.subscription_namespace = 'artcraft'
left outer join user_stripe_customer_links l
on l.user_token = u.token
and l.payments_namespace = 'artcraft'
where (us.maybe_stripe_customer_id IS NOT NULL OR l.stripe_customer_id IS NOT NULL)
order by u.created_at desc;
@@ -0,0 +1,5 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
DROP TABLE IF EXISTS user_email_changes;
@@ -0,0 +1,35 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE user_email_changes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The user whose email was changed.
user_token VARCHAR(32) NOT NULL,
-- The email address before the change.
old_email VARCHAR(255) NOT NULL,
-- The email address after the change.
new_email VARCHAR(255) NOT NULL,
-- The IP address the change was made from.
-- Wide enough for IPv4/6.
ip_address VARCHAR(40) NOT NULL,
-- The user that performed the change, if known. NULL when the change was
-- performed by the user themselves, by an automated process, or in any
-- other context where there is no acting user.
maybe_changed_by_user_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_user_token (user_token),
KEY index_old_email (old_email),
KEY index_new_email (new_email),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -0,0 +1,38 @@
-- NB: This is a manually squashed view of all the CREATE and ALTER statements,
-- with comments attached to the fields for centralized documentation.
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
CREATE TABLE user_email_changes (
id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
-- The user whose email was changed.
user_token VARCHAR(32) NOT NULL,
-- The email address before the change.
old_email VARCHAR(255) NOT NULL,
-- The email address after the change.
new_email VARCHAR(255) NOT NULL,
-- The IP address the change was made from.
-- Wide enough for IPv4/6.
ip_address VARCHAR(40) NOT NULL,
-- The user that performed the change, if known. NULL when the change was
-- performed by the user themselves, by an automated process, or in any
-- other context where there is no acting user.
maybe_changed_by_user_token VARCHAR(32) DEFAULT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- INDICES --
PRIMARY KEY (id),
KEY index_user_token (user_token),
KEY index_old_email (old_email),
KEY index_new_email (new_email),
KEY index_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
@@ -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.
@@ -71,7 +71,19 @@ pub struct OmniGenVideoModelDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub full_name: Option<String>,
/// Additional details about the model.
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_info: Option<String>,
/// Additional details about the model. (Brief; only a few words.)
#[serde(skip_serializing_if = "Option::is_none")]
pub extra_info_short: Option<String>,
/// Whether a pure text-to-video modality is supported
#[serde(skip_serializing_if = "Option::is_none")]
pub text_to_video_supported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub text_prompt_supported: Option<bool>,
@@ -183,6 +195,9 @@ impl Default for OmniGenVideoModelDetails {
model: DEFAULT_VIDEO_MODEL,
model_creator: None,
full_name: None,
extra_info: None,
extra_info_short: None,
text_to_video_supported: None,
text_prompt_supported: None,
text_prompt_max_length: None,
negative_text_prompt_supported: None,
@@ -22,7 +22,6 @@ artcraft_client.workspace = true
fal_client.workspace = true
gmicloud_client.workspace = true
grok_api_client.workspace = true
muapi_client.workspace = true
seedance2pro_client.workspace = true
tokens.workspace = true
url_utils.workspace = true
@@ -41,4 +40,5 @@ uuid.workspace = true
[dev-dependencies]
test_data.workspace = true
strum.workspace = true
serde_json.workspace = true
@@ -1,12 +1,12 @@
pub mod audio_list_ref;
pub mod character_list_ref;
pub mod common_aspect_ratio;
pub mod common_image_model;
pub mod common_quality;
pub mod common_resolution;
pub mod common_splat_model;
pub mod common_video_model;
pub mod image_list_ref;
pub mod image_ref;
pub mod provider;
pub mod router_aspect_ratio;
pub mod router_image_model;
pub mod router_provider;
pub mod router_quality;
pub mod router_resolution;
pub mod router_splat_model;
pub mod router_video_model;
pub mod video_list_ref;
@@ -8,7 +8,7 @@ use strum::EnumIter;
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(EnumIter))]
#[serde(rename_all = "snake_case")]
pub enum CommonAspectRatio {
pub enum RouterAspectRatio {
// Auto (eg. for image editing to use the source; used by Nano Banana Pro edit image, but not text-to-image)
Auto,
@@ -2,9 +2,9 @@ use serde_derive::{Deserialize, Serialize};
/// Common image models supported by the router.
/// Not all models are available through all providers.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonImageModel {
pub enum RouterImageModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
@@ -23,6 +23,12 @@ pub enum CommonImageModel {
GrokImagineImage,
#[serde(rename = "grok_imagine_image_q")]
GrokImagineImageQuality,
#[serde(rename = "midjourney_7")]
Midjourney7,
#[serde(rename = "midjourney_7_niji")]
Midjourney7Niji,
#[serde(rename = "midjourney_8")]
Midjourney8,
#[serde(rename = "nano_banana")]
NanoBanana,
#[serde(rename = "nano_banana_2")]
@@ -40,3 +46,31 @@ pub enum CommonImageModel {
#[serde(rename = "flux_2_lora_angles")]
Flux2LoraAngles,
}
#[cfg(test)]
mod tests {
use super::*;
fn assert_round_trip(model: RouterImageModel, expected_wire: &str) {
let json = serde_json::to_string(&model).unwrap();
let unquoted = json.trim_matches('"');
assert_eq!(unquoted, expected_wire, "serialize {:?}", model);
let back: RouterImageModel = serde_json::from_str(&json).unwrap();
assert_eq!(back, model, "deserialize round-trip for {:?}", model);
}
#[test]
fn midjourney_7_serializes_to_midjourney_7() {
assert_round_trip(RouterImageModel::Midjourney7, "midjourney_7");
}
#[test]
fn midjourney_7_niji_serializes_to_midjourney_7_niji() {
assert_round_trip(RouterImageModel::Midjourney7Niji, "midjourney_7_niji");
}
#[test]
fn midjourney_8_serializes_to_midjourney_8() {
assert_round_trip(RouterImageModel::Midjourney8, "midjourney_8");
}
}
@@ -3,11 +3,10 @@ use serde_derive::{Deserialize, Serialize};
/// The provider to route a generation request to.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Provider {
pub enum RouterProvider {
Artcraft,
Fal,
GmiCloud,
GrokApi,
Muapi,
Seedance2Pro,
}
@@ -4,7 +4,7 @@ use serde_derive::{Deserialize, Serialize};
/// Not every model will use this; models that don't simply ignore it.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CommonQuality {
pub enum RouterQuality {
High,
Medium,
Low,
@@ -5,7 +5,7 @@ use serde_derive::{Deserialize, Serialize};
/// In the case a model doesn't support the resolution, gracefully pick the nearest option.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonResolution {
pub enum RouterResolution {
/// Models: Nano Banana Pro
OneK,
/// Models: Nano Banana Pro
@@ -4,7 +4,7 @@ use serde_derive::{Deserialize, Serialize};
/// Not all models are available through all providers.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonSplatModel {
pub enum RouterSplatModel {
#[serde(rename = "marble_0p1_mini")]
Marble0p1Mini,
@@ -4,13 +4,16 @@ use serde_derive::{Deserialize, Serialize};
/// Not all models are available through all providers.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonVideoModel {
pub enum RouterVideoModel {
#[serde(rename = "grok_video")]
GrokVideo,
#[serde(rename = "grok_imagine_video")]
GrokImagineVideo,
#[serde(rename = "grok_imagine_video_1p5")]
GrokImagineVideo1p5,
#[serde(rename = "kling_1p6_pro")]
Kling16Pro,
@@ -56,6 +59,12 @@ pub enum CommonVideoModel {
#[serde(rename = "seedance_2p0_u_fast")]
Seedance2p0UltraFast,
#[serde(rename = "seedance_2p0_bpu")]
Seedance2p0BytePlusUltra,
#[serde(rename = "seedance_2p0_bpu_fast")]
Seedance2p0BytePlusUltraFast,
#[serde(rename = "happy_horse_1p0")]
HappyHorse1p0,
@@ -5,8 +5,6 @@ pub mod request_mismatch_mitigation_strategy;
pub mod router_artcraft_client;
pub mod router_client;
pub mod router_fal_client;
pub mod router_fal_webhook_optional_client;
pub mod router_gmicloud_client;
pub mod router_grok_api_client;
pub mod router_muapi_client;
pub mod router_seedance2pro_client;
@@ -1,19 +1,15 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::client_error::{ClientError, ClientType};
pub struct MultiRouterClient {
pub(crate) artcraft_client: Option<RouterArtcraftClient>,
pub(crate) fal_client: Option<RouterFalClient>,
pub(crate) fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
pub(crate) muapi_client: Option<RouterMuapiClient>,
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
}
@@ -28,11 +24,6 @@ impl MultiRouterClient {
.ok_or(ClientError::ClientNotConfigured(ClientType::Fal))
}
pub fn get_fal_webhook_optional_client_ref(&self) -> Result<&RouterFalWebhookOptionalClient, ClientError> {
self.fal_webhook_optional_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::FalWebhookOptional))
}
pub fn get_gmicloud_client_ref(&self) -> Result<&RouterGmiCloudClient, ClientError> {
self.gmicloud_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::GmiCloud))
@@ -43,11 +34,6 @@ impl MultiRouterClient {
.ok_or(ClientError::ClientNotConfigured(ClientType::GrokApi))
}
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
self.muapi_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Muapi))
}
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
self.seedance2pro_client.as_ref()
.ok_or(ClientError::ClientNotConfigured(ClientType::Seedance2Pro))
@@ -1,19 +1,15 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
pub struct MultiRouterClientBuilder {
artcraft_client: Option<RouterArtcraftClient>,
fal_client: Option<RouterFalClient>,
fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
gmicloud_client: Option<RouterGmiCloudClient>,
grok_api_client: Option<RouterGrokApiClient>,
muapi_client: Option<RouterMuapiClient>,
seedance2pro_client: Option<RouterSeedance2ProClient>,
}
@@ -22,10 +18,8 @@ impl MultiRouterClientBuilder {
Self {
artcraft_client: None,
fal_client: None,
fal_webhook_optional_client: None,
gmicloud_client: None,
grok_api_client: None,
muapi_client: None,
seedance2pro_client: None,
}
}
@@ -40,11 +34,6 @@ impl MultiRouterClientBuilder {
self
}
pub fn set_fal_webhook_optional_client(mut self, client: RouterFalWebhookOptionalClient) -> Self {
self.fal_webhook_optional_client = Some(client);
self
}
pub fn set_gmicloud_client(mut self, client: RouterGmiCloudClient) -> Self {
self.gmicloud_client = Some(client);
self
@@ -55,11 +44,6 @@ impl MultiRouterClientBuilder {
self
}
pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self {
self.muapi_client = Some(client);
self
}
pub fn set_seedance2pro_client(mut self, client: RouterSeedance2ProClient) -> Self {
self.seedance2pro_client = Some(client);
self
@@ -69,10 +53,8 @@ impl MultiRouterClientBuilder {
MultiRouterClient {
artcraft_client: self.artcraft_client,
fal_client: self.fal_client,
fal_webhook_optional_client: self.fal_webhook_optional_client,
gmicloud_client: self.gmicloud_client,
grok_api_client: self.grok_api_client,
muapi_client: self.muapi_client,
seedance2pro_client: self.seedance2pro_client,
}
}
@@ -1,10 +1,8 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
use crate::client::router_grok_api_client::RouterGrokApiClient;
use crate::client::router_muapi_client::RouterMuapiClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::client_error::{ClientError, ClientType};
@@ -12,10 +10,8 @@ pub enum RouterClient {
Multi(MultiRouterClient),
Artcraft(RouterArtcraftClient),
Fal(RouterFalClient),
FalWebhookOptional(RouterFalWebhookOptionalClient),
GmiCloud(RouterGmiCloudClient),
GrokApi(RouterGrokApiClient),
Muapi(RouterMuapiClient),
Seedance2Pro(RouterSeedance2ProClient),
}
@@ -36,14 +32,6 @@ impl RouterClient {
}
}
pub fn get_fal_webhook_optional_client_ref(&self) -> Result<&RouterFalWebhookOptionalClient, ClientError> {
match self {
RouterClient::FalWebhookOptional(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_fal_webhook_optional_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Fal)),
}
}
pub fn get_gmicloud_client_ref(&self) -> Result<&RouterGmiCloudClient, ClientError> {
match self {
RouterClient::GmiCloud(client) => Ok(client),
@@ -60,14 +48,6 @@ impl RouterClient {
}
}
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
match self {
RouterClient::Muapi(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_muapi_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Muapi)),
}
}
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ClientError> {
match self {
RouterClient::Seedance2Pro(client) => Ok(client),
@@ -1,19 +1,30 @@
use fal_client::creds::fal_api_key::FalApiKey;
/// A unified Fal client. The caller picks the dispatch mode:
///
/// - `Some(webhook_url)` → endpoints will use Fal's webhook flow.
/// - `None` → endpoints will use the queue/polling flow (where available).
///
/// Some endpoints are webhook-only (their fal_client wrapper lives under the
/// `requests_old::webhook::*` namespace and has no `api::` queue variant). Those
/// will return `ClientError::WebhookUrlRequired` when constructed with `None`.
pub struct RouterFalClient {
pub(crate) api_key: FalApiKey,
pub(crate) webhook_url: String,
pub(crate) webhook_url: Option<String>,
}
impl RouterFalClient {
pub fn new(api_key: FalApiKey, webhook_url: String) -> Self {
Self { api_key, webhook_url }
/// Build a client for webhook dispatch.
pub fn new_with_webhook(api_key: FalApiKey, webhook_url: String) -> Self {
Self { api_key, webhook_url: Some(webhook_url) }
}
pub fn new_from_raw_key(api_key: &str, webhook_url: String) -> Self {
Self {
api_key: FalApiKey::from_str(api_key),
webhook_url,
}
/// Build a client for queue/polling dispatch (no webhook URL).
pub fn new_polling_only(api_key: FalApiKey) -> Self {
Self { api_key, webhook_url: None }
}
pub fn new_polling_only_from_raw_key(api_key: &str) -> Self {
Self::new_polling_only(FalApiKey::from_str(api_key))
}
}
@@ -1,22 +0,0 @@
use fal_client::creds::fal_api_key::FalApiKey;
pub struct RouterFalWebhookOptionalClient {
pub(crate) api_key: FalApiKey,
pub(crate) webhook_url: Option<String>,
}
impl RouterFalWebhookOptionalClient {
pub fn new(api_key: FalApiKey) -> Self {
Self { api_key, webhook_url: None }
}
pub fn from_str(api_key: &str) -> Self {
Self::new(FalApiKey::from_str(api_key))
}
pub fn new_with_webhook(api_key: FalApiKey, webhook_url: String) -> Self {
Self {
api_key,
webhook_url: Some(webhook_url)
}
}
}
@@ -1,11 +0,0 @@
use muapi_client::creds::muapi_session::MuapiSession;
pub struct RouterMuapiClient {
pub(crate) session: MuapiSession,
}
impl RouterMuapiClient {
pub fn new(session: MuapiSession) -> Self {
RouterMuapiClient { session }
}
}
@@ -7,10 +7,8 @@ use tokens::tokens::media_files::MediaFileToken;
pub enum ClientType {
Artcraft,
Fal,
FalWebhookOptional,
GmiCloud,
GrokApi,
Muapi,
Seedance2Pro,
}
@@ -19,10 +17,8 @@ impl Display for ClientType {
match self {
Self::Artcraft => write!(f, "Artcraft"),
Self::Fal => write!(f, "Fal"),
Self::FalWebhookOptional => write!(f, "FalWebhookOptional"),
Self::GmiCloud => write!(f, "GmiCloud"),
Self::GrokApi => write!(f, "GrokApi"),
Self::Muapi => write!(f, "Muapi"),
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
}
}
@@ -49,11 +45,13 @@ pub enum ClientError {
/// Fal only accepts image URLs for image inputs, not media tokens.
FalOnlySupportsUrls,
/// Muapi only accepts URLs for image inputs, not media tokens.
MuapiOnlySupportsUrls,
/// Seedance2Pro only accepts URLs for media inputs, not media tokens.
Seedance2ProOnlySupportsUrls,
/// The Fal endpoint requires a webhook URL but the caller built the client
/// in polling/queue mode (no webhook URL). Returned by webhook-only
/// endpoints (those whose fal_client wrapper has no `api::` queue variant).
WebhookUrlRequired,
/// The pre-dispatch context of media file token to URL map was not supplied.
MediaFileToUrlMapNotProvided,
@@ -91,12 +89,12 @@ impl Display for ClientError {
Self::FalOnlySupportsUrls => {
write!(f, "Fal only supports image URLs for image inputs, not media tokens")
}
Self::MuapiOnlySupportsUrls => {
write!(f, "Muapi only supports URLs for image inputs; resolve media tokens to URLs before calling this provider")
}
Self::Seedance2ProOnlySupportsUrls => {
write!(f, "Seedance2Pro only supports URLs for media inputs; resolve media tokens to URLs before calling this provider")
}
Self::WebhookUrlRequired => {
write!(f, "This Fal endpoint only supports webhook dispatch; the caller built RouterFalClient in polling-only mode (no webhook URL)")
}
Self::MediaFileToUrlMapNotProvided => {
write!(f, "Media file to URL map was not provided")
}
@@ -4,7 +4,6 @@ use artcraft_client::error::storyteller_error::StorytellerError;
use fal_client::error::fal_error_plus::FalErrorPlus;
use gmicloud_client::error::gmicloud_error::GmiCloudError;
use grok_api_client::error::grok_error::GrokError;
use muapi_client::error::muapi_error::MuapiError;
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
#[derive(Debug)]
@@ -13,7 +12,6 @@ pub enum ProviderError {
Fal(FalErrorPlus),
GmiCloud(GmiCloudError),
Grok(GrokError),
Muapi(MuapiError),
Seedance2Pro(Seedance2ProError),
}
@@ -26,7 +24,6 @@ impl Display for ProviderError {
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
Self::Muapi(e) => write!(f, "Muapi provider error: {}", e),
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
}
}
@@ -56,12 +53,6 @@ impl From<GrokError> for ProviderError {
}
}
impl From<MuapiError> for ProviderError {
fn from(error: MuapiError) -> Self {
Self::Muapi(error)
}
}
impl From<Seedance2ProError> for ProviderError {
fn from(error: Seedance2ProError) -> Self {
Self::Seedance2Pro(error)
@@ -1,71 +0,0 @@
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
pub(crate) fn estimate_image_cost_artcraft_flux_1_dev(
plan: &PlanArtcraftFlux1Dev,
) -> ImageGenerationCostEstimate {
// Pricing: $0.02/image (2 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 2;
let num_images: u64 = match plan.num_images {
GenerateFlux1DevTextToImageNumImages::One => 1,
GenerateFlux1DevTextToImageNumImages::Two => 2,
GenerateFlux1DevTextToImageNumImages::Three => 3,
GenerateFlux1DevTextToImageNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.02/image = 2 cents each
assert_eq!(estimate_usd_cents(1), 2);
assert_eq!(estimate_usd_cents(2), 4);
assert_eq!(estimate_usd_cents(3), 6);
assert_eq!(estimate_usd_cents(4), 8);
}
}
@@ -1,50 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
pub(crate) fn estimate_image_cost_artcraft_flux_1_schnell(
_plan: &PlanArtcraftFlux1Schnell,
) -> ImageGenerationCostEstimate {
// Pricing: not available
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;
#[test]
fn test_estimate_cost_is_none() {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Flux1Schnell,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
let estimate = request.build()
.expect("build should succeed")
.estimate_costs();
assert!(estimate.cost_in_usd_cents.is_none());
assert!(estimate.cost_in_credits.is_none());
}
}
@@ -1,74 +0,0 @@
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
pub(crate) fn estimate_image_cost_artcraft_flux_2_lora_angles(
plan: &PlanArtcraftFlux2LoraAngles,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
Flux2LoraEditImageAngleNumImages::One => 1,
Flux2LoraEditImageAngleNumImages::Two => 2,
Flux2LoraEditImageAngleNumImages::Three => 3,
Flux2LoraEditImageAngleNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let tokens = vec![MediaFileToken::new_from_str("test_token")];
let request = GenerateImageRequestBuilder {
model: CommonImageModel::Flux2LoraAngles,
provider: Provider::Artcraft,
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
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,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,74 +0,0 @@
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
plan: &PlanArtcraftFluxPro11,
) -> ImageGenerationCostEstimate {
// Fal pricing: $0.04 per megapixel, billed by rounding up to the nearest
// megapixel. The default image_size values are ~1MP, so 4 cents per image.
// (The legacy `generate_flux_pro_11_text_to_image_handler` charges this same
// amount via `FluxPro11Args::calculate_cost_in_cents`.)
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
GenerateFluxPro11TextToImageNumImages::One => 1,
GenerateFluxPro11TextToImageNumImages::Two => 2,
GenerateFluxPro11TextToImageNumImages::Three => 3,
GenerateFluxPro11TextToImageNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each (Fal's $0.04/MP at the default ~1MP).
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,71 +0,0 @@
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1_ultra(
plan: &PlanArtcraftFluxPro11Ultra,
) -> ImageGenerationCostEstimate {
// Pricing: $0.06/image (6 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 6;
let num_images: u64 = match plan.num_images {
GenerateFluxPro11UltraTextToImageNumImages::One => 1,
GenerateFluxPro11UltraTextToImageNumImages::Two => 2,
GenerateFluxPro11UltraTextToImageNumImages::Three => 3,
GenerateFluxPro11UltraTextToImageNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.06/image = 6 cents each
assert_eq!(estimate_usd_cents(1), 6);
assert_eq!(estimate_usd_cents(2), 12);
assert_eq!(estimate_usd_cents(3), 18);
assert_eq!(estimate_usd_cents(4), 24);
}
}
@@ -1,240 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::{
ArtcraftGptImage1Quality, ArtcraftGptImage1Size, PlanArtcraftGptImage1,
};
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1(
plan: &PlanArtcraftGptImage1,
) -> ImageGenerationCostEstimate {
// Pricing per the fal-ai GPT Image 1 docs, adapted for our artcraft billing:
//
// Output image cost (per output image):
// Low – $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢
// Medium – $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢
// High – $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢
//
// Input image cost (edit mode only, per input image, high-fidelity assumed):
// 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image
//
// Input text tokens: free (we don't charge for these).
//
let is_square = matches!(
plan.image_size,
None | Some(ArtcraftGptImage1Size::Square)
);
let output_cost_per_image: u64 = match (plan.quality, is_square) {
(ArtcraftGptImage1Quality::Auto, _) => 17, // Auto treated as High square
(ArtcraftGptImage1Quality::Low, true) => 2,
(ArtcraftGptImage1Quality::Low, false) => 2,
(ArtcraftGptImage1Quality::Medium, true) => 5,
(ArtcraftGptImage1Quality::Medium, false) => 7,
(ArtcraftGptImage1Quality::High, true) => 17,
(ArtcraftGptImage1Quality::High, false) => 25,
};
let num_output_images = plan.num_images.as_u64();
let output_cost = output_cost_per_image * num_output_images;
// Input image token cost: 2¢ per input image (high-fidelity estimate).
let input_image_cost: u64 = 2 * plan.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,
}
}
#[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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
}
}
fn estimate_text(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
) -> u64 {
let request = GenerateImageRequestBuilder {
quality,
aspect_ratio,
image_batch_count: Some(batch),
..base()
};
request.build().unwrap().estimate_costs()
.cost_in_usd_cents.unwrap()
}
fn estimate_edit(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
num_input_images: usize,
) -> u64 {
let tokens: Vec<MediaFileToken> = (0..num_input_images)
.map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i)))
.collect();
let request = GenerateImageRequestBuilder {
quality,
aspect_ratio,
image_batch_count: Some(batch),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base()
};
request.build().unwrap().estimate_costs()
.cost_in_usd_cents.unwrap()
}
// ── Default quality (None → High) ───────────────────────────────────────
#[test]
fn default_quality_square_one_image_costs_17_cents() {
assert_eq!(estimate_text(None, None, 1), 17);
}
#[test]
fn default_quality_square_four_images_costs_68_cents() {
assert_eq!(estimate_text(None, None, 4), 68);
}
// ── Low quality (2¢/image regardless of size) ───────────────────────────
#[test]
fn low_square_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 2);
}
#[test]
fn low_wide_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
}
#[test]
fn low_tall_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
}
#[test]
fn low_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::Low), None, 4), 8);
}
// ── Medium quality (5¢ square, 7¢ wide/tall) ───────────────────────────
#[test]
fn medium_square_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 5);
}
#[test]
fn medium_wide_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 7);
}
#[test]
fn medium_tall_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 7);
}
#[test]
fn medium_square_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 20);
}
#[test]
fn medium_wide_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 28);
}
// ── High quality (17¢ square, 25¢ wide/tall) ───────────────────────────
#[test]
fn high_square_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
}
#[test]
fn high_wide_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 25);
}
#[test]
fn high_tall_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 25);
}
#[test]
fn high_square_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 68);
}
#[test]
fn high_wide_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 100);
}
// ── Edit mode: adds 2¢ per input image ─────────────────────────────────
#[test]
fn edit_one_input_adds_2_cents() {
// High square 1 output = 17¢, plus 1 input × 2¢ = 19¢
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 1), 19);
}
#[test]
fn edit_three_inputs_adds_6_cents() {
// High square 1 output = 17¢, plus 3 inputs × 2¢ = 23¢
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 3), 23);
}
#[test]
fn edit_five_inputs_adds_10_cents() {
// Medium wide 2 outputs = 7×2 = 14¢, plus 5 inputs × 2¢ = 24¢
assert_eq!(estimate_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 2, 5), 24);
}
#[test]
fn edit_low_quality_with_inputs() {
// Low square 1 output = 2¢, plus 2 inputs × 2¢ = 6¢
assert_eq!(estimate_edit(Some(CommonQuality::Low), None, 1, 2), 6);
}
// ── Text mode (no input images) has no input cost ──────────────────────
#[test]
fn text_mode_no_input_cost() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
}
}
@@ -1,227 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality,
GptImage1p5MultiFunctionImageGenSize,
};
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
plan: &PlanArtcraftGptImage1p5,
) -> ImageGenerationCostEstimate {
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
//
// Output image cost (per output image), rounded up to whole cents:
// Low: $0.009 (1024×1024) / $0.013 (other) → 1¢ / 2¢
// Medium: $0.034 (1024×1024) / $0.050 (1536×1024) / $0.051 (1024×1536) → 4¢ / 5¢ / 6¢
// High: $0.133 (1024×1024) / $0.199 (1536×1024) / $0.200 (1024×1536) → 14¢ / 20¢ / 20¢
//
// Input text tokens: free (we don't charge for these).
//
// Default quality: High (when request.quality is None).
//
use GptImage1p5MultiFunctionImageGenQuality as Q;
use GptImage1p5MultiFunctionImageGenSize as S;
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
// Low quality
(Q::Low, None | Some(S::Square)) => 1,
(Q::Low, Some(S::Wide) | Some(S::Tall)) => 2,
// Medium quality
(Q::Medium, None | Some(S::Square)) => 4,
(Q::Medium, Some(S::Wide)) => 5,
(Q::Medium, Some(S::Tall)) => 6,
// High quality
(Q::High, None | Some(S::Square)) => 14,
(Q::High, Some(S::Wide) | Some(S::Tall)) => 20,
};
let num_images: u64 = match plan.num_images {
GptImage1p5MultiFunctionImageGenNumImages::One => 1,
GptImage1p5MultiFunctionImageGenNumImages::Two => 2,
GptImage1p5MultiFunctionImageGenNumImages::Three => 3,
GptImage1p5MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
) -> u64 {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1p5,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio,
quality,
image_batch_count: Some(batch),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build().unwrap().estimate_costs()
.cost_in_usd_cents.unwrap()
}
// ── Default quality (None → High) ─────────────────────────────────────
#[test]
fn default_quality_square_one_image() {
assert_eq!(estimate(None, Some(CommonAspectRatio::Square), 1), 14);
}
#[test]
fn default_quality_unset_one_image() {
assert_eq!(estimate(None, None, 1), 14);
}
#[test]
fn default_quality_wide_one_image() {
assert_eq!(estimate(None, Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
}
#[test]
fn default_quality_tall_one_image() {
assert_eq!(estimate(None, Some(CommonAspectRatio::TallNineBySixteen), 1), 20);
}
#[test]
fn default_quality_square_four_images() {
assert_eq!(estimate(None, Some(CommonAspectRatio::Square), 4), 56);
}
// ── Low quality (1¢ square, 2¢ wide/tall) ─────────────────────────────
#[test]
fn low_square_one() {
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 1);
}
#[test]
fn low_unset_one() {
assert_eq!(estimate(Some(CommonQuality::Low), None, 1), 1);
}
#[test]
fn low_wide_one() {
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
}
#[test]
fn low_tall_one() {
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
}
#[test]
fn low_square_four() {
assert_eq!(estimate(Some(CommonQuality::Low), None, 4), 4);
}
#[test]
fn low_wide_four() {
assert_eq!(estimate(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 4), 8);
}
// ── Medium quality (4¢ square, 5¢ wide, 6¢ tall) ─────────────────────
#[test]
fn medium_square_one() {
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 4);
}
#[test]
fn medium_unset_one() {
assert_eq!(estimate(Some(CommonQuality::Medium), None, 1), 4);
}
#[test]
fn medium_wide_one() {
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 5);
}
#[test]
fn medium_tall_one() {
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 6);
}
#[test]
fn medium_square_four() {
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 16);
}
#[test]
fn medium_wide_four() {
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 20);
}
#[test]
fn medium_tall_four() {
assert_eq!(estimate(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 4), 24);
}
// ── High quality (14¢ square, 20¢ wide/tall) ─────────────────────────
#[test]
fn high_square_one() {
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 14);
}
#[test]
fn high_unset_one() {
assert_eq!(estimate(Some(CommonQuality::High), None, 1), 14);
}
#[test]
fn high_wide_one() {
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
}
#[test]
fn high_tall_one() {
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 20);
}
#[test]
fn high_square_four() {
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 56);
}
#[test]
fn high_wide_four() {
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 80);
}
#[test]
fn high_tall_four() {
assert_eq!(estimate(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 4), 80);
}
}
@@ -1,211 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::{
ArtcraftGptImage2ImageSize, ArtcraftGptImage2Quality, PlanArtcraftGptImage2,
};
pub(crate) fn estimate_image_cost_artcraft_gpt_image_2(
plan: &PlanArtcraftGptImage2,
) -> ImageGenerationCostEstimate {
// Same cost table as the Fal GPT Image 2 estimator.
//
// Output image cost (per output image), rounded up to whole cents:
// Low: all sizes → 1¢
// Medium: landscape_4_3/portrait_4_3 → 4¢
// landscape_16_9/portrait_16_9 → 4¢
// square → 6¢
// square_hd → 6¢
// High: landscape_4_3/portrait_4_3 → 15¢
// landscape_16_9/portrait_16_9 → 16¢
// square → 22¢
// square_hd → 23¢
//
use ArtcraftGptImage2ImageSize as S;
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
// Low quality
(ArtcraftGptImage2Quality::Low, _) => 1,
// Medium quality
(ArtcraftGptImage2Quality::Medium, None | Some(S::Square) | Some(S::SquareHd)) => 6,
(ArtcraftGptImage2Quality::Medium, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 4,
(ArtcraftGptImage2Quality::Medium, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 4,
(ArtcraftGptImage2Quality::Medium, Some(S::Auto)) => 6,
// High quality
(ArtcraftGptImage2Quality::High, None | Some(S::Square)) => 22,
(ArtcraftGptImage2Quality::High, Some(S::SquareHd)) => 23,
(ArtcraftGptImage2Quality::High, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 15,
(ArtcraftGptImage2Quality::High, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 16,
(ArtcraftGptImage2Quality::High, Some(S::Auto)) => 23,
};
let cost_in_usd_cents = cost_per_image * plan.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::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::ArtcraftGptImage2NumImages;
fn plan(
quality: ArtcraftGptImage2Quality,
image_size: Option<ArtcraftGptImage2ImageSize>,
num_images: ArtcraftGptImage2NumImages,
) -> PlanArtcraftGptImage2 {
PlanArtcraftGptImage2 {
prompt: Some("p".to_string()),
image_inputs: None,
image_size,
quality,
num_images,
idempotency_token: "test".to_string(),
}
}
fn cents(
quality: ArtcraftGptImage2Quality,
image_size: Option<ArtcraftGptImage2ImageSize>,
num_images: ArtcraftGptImage2NumImages,
) -> u64 {
estimate_image_cost_artcraft_gpt_image_2(&plan(quality, image_size, num_images))
.cost_in_usd_cents.unwrap()
}
// -- Low quality (1c for all sizes) -------------------------------------------
#[test]
fn low_square() {
assert_eq!(cents(ArtcraftGptImage2Quality::Low, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::One), 1);
}
#[test]
fn low_square_hd() {
assert_eq!(cents(ArtcraftGptImage2Quality::Low, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::One), 1);
}
#[test]
fn low_unset() {
assert_eq!(cents(ArtcraftGptImage2Quality::Low, None, ArtcraftGptImage2NumImages::One), 1);
}
#[test]
fn low_landscape_4x3() {
assert_eq!(cents(ArtcraftGptImage2Quality::Low, Some(ArtcraftGptImage2ImageSize::Landscape4x3), ArtcraftGptImage2NumImages::One), 1);
}
#[test]
fn low_four() {
assert_eq!(cents(ArtcraftGptImage2Quality::Low, None, ArtcraftGptImage2NumImages::Four), 4);
}
// -- Medium quality -----------------------------------------------------------
#[test]
fn medium_square() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::One), 6);
}
#[test]
fn medium_square_hd() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::One), 6);
}
#[test]
fn medium_unset() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, None, ArtcraftGptImage2NumImages::One), 6);
}
#[test]
fn medium_landscape_4x3() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Landscape4x3), ArtcraftGptImage2NumImages::One), 4);
}
#[test]
fn medium_portrait_4x3() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Portrait4x3), ArtcraftGptImage2NumImages::One), 4);
}
#[test]
fn medium_landscape_16x9() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Landscape16x9), ArtcraftGptImage2NumImages::One), 4);
}
#[test]
fn medium_portrait_16x9() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Portrait16x9), ArtcraftGptImage2NumImages::One), 4);
}
#[test]
fn medium_square_four() {
assert_eq!(cents(ArtcraftGptImage2Quality::Medium, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::Four), 24);
}
// -- High quality -------------------------------------------------------------
#[test]
fn high_square() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::One), 22);
}
#[test]
fn high_square_hd() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::One), 23);
}
#[test]
fn high_unset() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, None, ArtcraftGptImage2NumImages::One), 22);
}
#[test]
fn high_landscape_4x3() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Landscape4x3), ArtcraftGptImage2NumImages::One), 15);
}
#[test]
fn high_portrait_4x3() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Portrait4x3), ArtcraftGptImage2NumImages::One), 15);
}
#[test]
fn high_landscape_16x9() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Landscape16x9), ArtcraftGptImage2NumImages::One), 16);
}
#[test]
fn high_portrait_16x9() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Portrait16x9), ArtcraftGptImage2NumImages::One), 16);
}
#[test]
fn high_square_four() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::Square), ArtcraftGptImage2NumImages::Four), 88);
}
#[test]
fn high_square_hd_four() {
assert_eq!(cents(ArtcraftGptImage2Quality::High, Some(ArtcraftGptImage2ImageSize::SquareHd), ArtcraftGptImage2NumImages::Four), 92);
}
// -- Metadata flags -----------------------------------------------------------
#[test]
fn metadata_flags_are_default() {
let estimate = estimate_image_cost_artcraft_gpt_image_2(&plan(
ArtcraftGptImage2Quality::Medium, None, ArtcraftGptImage2NumImages::One,
));
assert!(!estimate.is_free);
assert!(!estimate.is_unlimited);
assert!(!estimate.is_rate_limited);
assert!(!estimate.has_watermark);
}
}
@@ -1,71 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::NanoBananaMultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
pub(crate) fn estimate_image_cost_artcraft_nano_banana(
plan: &PlanArtcraftNanaBanana,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
NanoBananaMultiFunctionImageGenNumImages::One => 1,
NanoBananaMultiFunctionImageGenNumImages::Two => 2,
NanoBananaMultiFunctionImageGenNumImages::Three => 3,
NanoBananaMultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,197 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
NanaBanana2MultiFunctionImageGenImageResolution, NanaBanana2MultiFunctionImageGenNumImages,
};
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
plan: &PlanArtcraftNanaBanana2,
) -> ImageGenerationCostEstimate {
// Pricing (USD cents per image), scaled off the 1K base cost of $0.08:
// 0.5K → $0.06 (0.75×)
// 1K → $0.08 (1×, default)
// 2K → $0.12 (1.5×)
// 4K → $0.16 (2×)
let cost_per_image: u64 = match plan.resolution {
Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 6,
Some(NanaBanana2MultiFunctionImageGenImageResolution::OneK) => 8,
Some(NanaBanana2MultiFunctionImageGenImageResolution::TwoK) => 12,
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 16,
None => 8, // default 1K
};
let num_images: u64 = match plan.num_images {
NanaBanana2MultiFunctionImageGenNumImages::One => 1,
NanaBanana2MultiFunctionImageGenNumImages::Two => 2,
NanaBanana2MultiFunctionImageGenNumImages::Three => 3,
NanaBanana2MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
// ── Default (None → 1K → 8¢/image) ─────────────────────────────────────────
#[test]
fn default_resolution_one_image_costs_8_cents() {
assert_eq!(estimate_usd_cents(None, 1), 8);
}
#[test]
fn default_resolution_two_images_costs_16_cents() {
assert_eq!(estimate_usd_cents(None, 2), 16);
}
#[test]
fn default_resolution_three_images_costs_24_cents() {
assert_eq!(estimate_usd_cents(None, 3), 24);
}
#[test]
fn default_resolution_four_images_costs_32_cents() {
assert_eq!(estimate_usd_cents(None, 4), 32);
}
// ── 0.5K → 6¢/image (0.75× base) ───────────────────────────────────────────
#[test]
fn half_k_one_image_costs_6_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 1), 6);
}
#[test]
fn half_k_two_images_costs_12_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 2), 12);
}
#[test]
fn half_k_three_images_costs_18_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 3), 18);
}
#[test]
fn half_k_four_images_costs_24_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::HalfK), 4), 24);
}
// ── 1K → 8¢/image (base) ───────────────────────────────────────────────────
#[test]
fn one_k_one_image_costs_8_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 8);
}
#[test]
fn one_k_two_images_costs_16_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 16);
}
#[test]
fn one_k_three_images_costs_24_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 24);
}
#[test]
fn one_k_four_images_costs_32_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 32);
}
// ── 2K → 12¢/image (1.5× base) ─────────────────────────────────────────────
#[test]
fn two_k_one_image_costs_12_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 12);
}
#[test]
fn two_k_two_images_costs_24_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 2), 24);
}
#[test]
fn two_k_three_images_costs_36_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 3), 36);
}
#[test]
fn two_k_four_images_costs_48_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 4), 48);
}
// ── 4K → 16¢/image (2× base) ───────────────────────────────────────────────
#[test]
fn four_k_one_image_costs_16_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 16);
}
#[test]
fn four_k_two_images_costs_32_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 32);
}
#[test]
fn four_k_three_images_costs_48_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 3), 48);
}
#[test]
fn four_k_four_images_costs_64_cents() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 64);
}
// ── 3K (unsupported) falls back to 2K pricing ──────────────────────────────
#[test]
fn three_k_one_image_falls_back_to_two_k_pricing() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::ThreeK), 1), 12);
}
#[test]
fn three_k_four_images_falls_back_to_two_k_pricing() {
assert_eq!(estimate_usd_cents(Some(CommonResolution::ThreeK), 4), 48);
}
}
@@ -1,85 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages,
};
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
pub(crate) fn estimate_image_cost_artcraft_nano_banana_pro(
plan: &PlanArtcraftNanaBananaPro,
) -> ImageGenerationCostEstimate {
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
// (Stored as USD cents.)
let cost_per_image: u64 = match plan.resolution {
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => 30,
_ => 15,
};
let num_images: u64 = match plan.num_images {
NanoBananaProMultiFunctionImageGenNumImages::One => 1,
NanoBananaProMultiFunctionImageGenNumImages::Two => 2,
NanoBananaProMultiFunctionImageGenNumImages::Three => 3,
NanoBananaProMultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// 1K/2K: $0.15/image = 15 cents
assert_eq!(estimate_usd_cents(None, 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 45);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
// 4K: $0.30/image = 30 cents
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 120);
}
}
@@ -1,74 +0,0 @@
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
pub(crate) fn estimate_image_cost_artcraft_qwen_edit_2511_angles(
plan: &PlanArtcraftQwenEdit2511Angles,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
QwenEdit2511EditImageAngleNumImages::One => 1,
QwenEdit2511EditImageAngleNumImages::Two => 2,
QwenEdit2511EditImageAngleNumImages::Three => 3,
QwenEdit2511EditImageAngleNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let tokens = vec![MediaFileToken::new_from_str("test_token")];
let request = GenerateImageRequestBuilder {
model: CommonImageModel::QwenEdit2511Angles,
provider: Provider::Artcraft,
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,71 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::BytedanceSeedreamV4MultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
pub(crate) fn estimate_image_cost_artcraft_seedream_4(
plan: &PlanArtcraftSeedream4,
) -> ImageGenerationCostEstimate {
// Pricing: $0.03/image (3 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 3;
let num_images: u64 = match plan.num_images {
BytedanceSeedreamV4MultiFunctionImageGenNumImages::One => 1,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.03/image = 3 cents each
assert_eq!(estimate_usd_cents(1), 3);
assert_eq!(estimate_usd_cents(2), 6);
assert_eq!(estimate_usd_cents(3), 9);
assert_eq!(estimate_usd_cents(4), 12);
}
}
@@ -1,71 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::BytedanceSeedreamV4p5MultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
pub(crate) fn estimate_image_cost_artcraft_seedream_4p5(
plan: &PlanArtcraftSeedream4p5,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One => 1,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,71 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::BytedanceSeedream5LiteMultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
pub(crate) fn estimate_image_cost_artcraft_seedream_5_lite(
plan: &PlanArtcraftSeedream5Lite,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::One => 1,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = 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),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,15 +0,0 @@
pub mod estimate_image_cost_artcraft_flux_1_dev;
pub mod estimate_image_cost_artcraft_flux_1_schnell;
pub mod estimate_image_cost_artcraft_flux_pro_1p1;
pub mod estimate_image_cost_artcraft_flux_pro_1p1_ultra;
pub mod estimate_image_cost_artcraft_gpt_image_1;
pub mod estimate_image_cost_artcraft_gpt_image_1p5;
pub mod estimate_image_cost_artcraft_gpt_image_2;
pub mod estimate_image_cost_artcraft_nano_banana;
pub mod estimate_image_cost_artcraft_nano_banana_2;
pub mod estimate_image_cost_artcraft_nano_banana_pro;
pub mod estimate_image_cost_artcraft_seedream_4;
pub mod estimate_image_cost_artcraft_seedream_4p5;
pub mod estimate_image_cost_artcraft_seedream_5_lite;
pub mod estimate_image_cost_artcraft_qwen_edit_2511_angles;
pub mod estimate_image_cost_artcraft_flux_2_lora_angles;
@@ -1,27 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
pub(crate) fn estimate_image_cost_fal_flux_1_dev(
plan: &PlanFalFlux1Dev,
) -> ImageGenerationCostEstimate {
// Pricing: $0.025 per megapixel, billed by rounding up to nearest megapixel.
// Default image sizes are ~1MP (1024x1024), so ~$0.025/image = 3 cents rounded up.
//
// Fal docs:
// Your request will cost $0.025 per megapixel, with images billed by rounding up
// to the nearest megapixel.
//
let cost_per_image: u64 = 3;
let num_images = plan.num_images.as_u64();
let cost_in_usd_cents = cost_per_image * 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,
}
}
@@ -1,27 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
pub(crate) fn estimate_image_cost_fal_flux_1_schnell(
plan: &PlanFalFlux1Schnell,
) -> ImageGenerationCostEstimate {
// Pricing: $0.003 per megapixel, billed by rounding up to nearest megapixel.
// Default image sizes are ~1MP (1024x1024), so ~$0.003/image = 1 cent rounded up.
//
// Fal docs:
// Your request will cost $0.003 per megapixel, with images billed by rounding up
// to the nearest megapixel.
//
let cost_per_image: u64 = 1;
let num_images = plan.num_images.as_u64();
let cost_in_usd_cents = cost_per_image * 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,
}
}
@@ -1,22 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1(
plan: &PlanFalFluxPro11,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04 per megapixel, billed by rounding up to nearest megapixel.
// Default image sizes are ~1MP (1024x1024), so ~$0.04/image = 4 cents.
let cost_per_image: u64 = 4;
let num_images = plan.num_images.as_u64();
let cost_in_usd_cents = cost_per_image * 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,
}
}
@@ -1,21 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
pub(crate) fn estimate_image_cost_fal_flux_pro_1p1_ultra(
plan: &PlanFalFluxPro11Ultra,
) -> ImageGenerationCostEstimate {
// Pricing: $0.06 per image.
let cost_per_image: u64 = 6;
let num_images = plan.num_images.as_u64();
let cost_in_usd_cents = cost_per_image * 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,
}
}
@@ -1,246 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::{
FalGptImage1ImageSize, FalGptImage1Quality, PlanFalGptImage1,
};
pub(crate) fn estimate_image_cost_fal_gpt_image_1(
plan: &PlanFalGptImage1,
) -> ImageGenerationCostEstimate {
// Per fal docs (fal-ai/gpt-image-1/{text-to-image,edit-image}):
//
// Output image cost (per output image):
// Low: $0.011 (1024×1024) / $0.016 (other) → 2¢ / 2¢
// Medium: $0.042 (1024×1024) / $0.063 (other) → 5¢ / 7¢
// High: $0.167 (1024×1024) / $0.250 (other) → 17¢ / 25¢
//
// Input image tokens (edit mode, per input image, high-fidelity assumed):
// 3,050 tokens × $0.005/1K tokens ≈ $0.01525 → 2¢ per input image
//
// Input text tokens: free (we don't charge for these).
//
let is_square = matches!(plan.image_size, None | Some(FalGptImage1ImageSize::Square));
let output_cost_per_image: u64 = match (plan.quality, is_square) {
(FalGptImage1Quality::Low, true) => 2,
(FalGptImage1Quality::Low, false) => 2,
(FalGptImage1Quality::Medium, true) => 5,
(FalGptImage1Quality::Medium, false) => 7,
(FalGptImage1Quality::High, true) => 17,
(FalGptImage1Quality::High, false) => 25,
};
let num_output_images = plan.num_images.as_u64();
let output_cost = output_cost_per_image * num_output_images;
// Input image token cost: 2¢ per input image (high-fidelity estimate).
let input_image_cost: u64 = if plan.image_urls.is_empty() {
0
} else {
2 * plan.image_urls.len() as u64
};
let cost_in_usd_cents = output_cost + input_image_cost;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
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::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::FalGptImage1NumImages;
fn text_plan(
quality: FalGptImage1Quality,
image_size: Option<FalGptImage1ImageSize>,
num_images: FalGptImage1NumImages,
) -> PlanFalGptImage1 {
PlanFalGptImage1 {
prompt: Some("p".to_string()),
image_urls: vec![],
image_size,
quality,
num_images,
}
}
fn edit_plan(
quality: FalGptImage1Quality,
image_size: Option<FalGptImage1ImageSize>,
num_images: FalGptImage1NumImages,
num_input_images: usize,
) -> PlanFalGptImage1 {
let image_urls = (0..num_input_images)
.map(|i| format!("https://fake.example.com/img{}.png", i))
.collect();
PlanFalGptImage1 {
prompt: Some("p".to_string()),
image_urls,
image_size,
quality,
num_images,
}
}
fn cents_text(
quality: FalGptImage1Quality,
image_size: Option<FalGptImage1ImageSize>,
num_images: FalGptImage1NumImages,
) -> u64 {
estimate_image_cost_fal_gpt_image_1(&text_plan(quality, image_size, num_images))
.cost_in_usd_cents
.unwrap()
}
fn cents_edit(
quality: FalGptImage1Quality,
image_size: Option<FalGptImage1ImageSize>,
num_images: FalGptImage1NumImages,
num_input_images: usize,
) -> u64 {
estimate_image_cost_fal_gpt_image_1(&edit_plan(quality, image_size, num_images, num_input_images))
.cost_in_usd_cents
.unwrap()
}
// ── Low quality text-to-image (2¢/image, both sizes) ──────────────────
#[test]
fn low_square_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 2);
}
#[test]
fn low_horizontal_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 2);
}
#[test]
fn low_vertical_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 2);
}
#[test]
fn low_unset_size_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::One), 2);
}
#[test]
fn low_four_images() {
assert_eq!(cents_text(FalGptImage1Quality::Low, None, FalGptImage1NumImages::Four), 8);
}
// ── Medium quality text-to-image (5¢ square, 7¢ wide/tall) ────────────
#[test]
fn medium_square_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 5);
}
#[test]
fn medium_horizontal_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 7);
}
#[test]
fn medium_vertical_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 7);
}
#[test]
fn medium_square_four_images() {
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Four), 20);
}
#[test]
fn medium_horizontal_four_images() {
assert_eq!(cents_text(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 28);
}
// ── High quality text-to-image (17¢ square, 25¢ wide/tall) ────────────
#[test]
fn high_square_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17);
}
#[test]
fn high_horizontal_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::One), 25);
}
#[test]
fn high_vertical_one_image() {
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::One), 25);
}
#[test]
fn high_square_two_images() {
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::Two), 34);
}
#[test]
fn high_horizontal_four_images() {
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Four), 100);
}
// ── Edit mode adds 2¢ per input image ─────────────────────────────────
#[test]
fn edit_high_square_one_output_one_input() {
// 17¢ output + 2¢ input = 19¢
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 1), 19);
}
#[test]
fn edit_high_square_one_output_three_inputs() {
// 17¢ output + 3×2¢ input = 23¢
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 3), 23);
}
#[test]
fn edit_medium_wide_two_outputs_five_inputs() {
// 2×7¢ output + 5×2¢ input = 24¢
assert_eq!(cents_edit(FalGptImage1Quality::Medium, Some(FalGptImage1ImageSize::Horizontal), FalGptImage1NumImages::Two, 5), 24);
}
#[test]
fn edit_low_square_one_output_two_inputs() {
// 2¢ output + 2×2¢ input = 6¢
assert_eq!(cents_edit(FalGptImage1Quality::Low, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One, 2), 6);
}
#[test]
fn edit_high_vertical_four_outputs_one_input() {
// 4×25¢ output + 1×2¢ input = 102¢
assert_eq!(cents_edit(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Vertical), FalGptImage1NumImages::Four, 1), 102);
}
// ── Text mode has no input image cost ─────────────────────────────────
#[test]
fn text_mode_no_input_cost() {
assert_eq!(cents_text(FalGptImage1Quality::High, Some(FalGptImage1ImageSize::Square), FalGptImage1NumImages::One), 17);
}
// ── Metadata flags ────────────────────────────────────────────────────
#[test]
fn metadata_flags_are_default() {
let estimate = estimate_image_cost_fal_gpt_image_1(&text_plan(
FalGptImage1Quality::Medium,
None,
FalGptImage1NumImages::One,
));
assert!(!estimate.is_free);
assert!(!estimate.is_unlimited);
assert!(!estimate.is_rate_limited);
assert!(!estimate.has_watermark);
}
}
@@ -1,181 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::{
FalGptImage1p5ImageSize, FalGptImage1p5Quality, PlanFalGptImage1p5,
};
pub(crate) fn estimate_image_cost_fal_gpt_image_1p5(
plan: &PlanFalGptImage1p5,
) -> ImageGenerationCostEstimate {
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
//
// Output image cost (per output image), rounded up to whole cents:
// Low: $0.009 (1024×1024) / $0.013 (other) → 1¢ / 2¢
// Medium: $0.034 (1024×1024) / $0.050 (1536×1024) / $0.051 (1024×1536) → 4¢ / 5¢ / 6¢
// High: $0.133 (1024×1024) / $0.199 (1536×1024) / $0.200 (1024×1536) → 14¢ / 20¢ / 20¢
//
use FalGptImage1p5ImageSize as S;
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
// Low quality
(FalGptImage1p5Quality::Low, None | Some(S::Square)) => 1,
(FalGptImage1p5Quality::Low, Some(S::Wide) | Some(S::Tall)) => 2,
// Medium quality
(FalGptImage1p5Quality::Medium, None | Some(S::Square)) => 4,
(FalGptImage1p5Quality::Medium, Some(S::Wide)) => 5,
(FalGptImage1p5Quality::Medium, Some(S::Tall)) => 6,
// High quality
(FalGptImage1p5Quality::High, None | Some(S::Square)) => 14,
(FalGptImage1p5Quality::High, Some(S::Wide) | Some(S::Tall)) => 20,
};
let cost_in_usd_cents = cost_per_image * plan.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::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::FalGptImage1p5NumImages;
fn plan(
quality: FalGptImage1p5Quality,
image_size: Option<FalGptImage1p5ImageSize>,
num_images: FalGptImage1p5NumImages,
) -> PlanFalGptImage1p5 {
PlanFalGptImage1p5 {
prompt: Some("p".to_string()),
image_urls: vec![],
image_size,
quality,
num_images,
}
}
fn cents(
quality: FalGptImage1p5Quality,
image_size: Option<FalGptImage1p5ImageSize>,
num_images: FalGptImage1p5NumImages,
) -> u64 {
estimate_image_cost_fal_gpt_image_1p5(&plan(quality, image_size, num_images))
.cost_in_usd_cents.unwrap()
}
// ── Low quality (1¢ square, 2¢ wide/tall) ─────────────────────────────
#[test]
fn low_square() {
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 1);
}
#[test]
fn low_unset() {
assert_eq!(cents(FalGptImage1p5Quality::Low, None, FalGptImage1p5NumImages::One), 1);
}
#[test]
fn low_wide() {
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 2);
}
#[test]
fn low_tall() {
assert_eq!(cents(FalGptImage1p5Quality::Low, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 2);
}
#[test]
fn low_four() {
assert_eq!(cents(FalGptImage1p5Quality::Low, None, FalGptImage1p5NumImages::Four), 4);
}
// ── Medium quality (4¢ square, 5¢ wide, 6¢ tall) ─────────────────────
#[test]
fn medium_square() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 4);
}
#[test]
fn medium_unset() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, None, FalGptImage1p5NumImages::One), 4);
}
#[test]
fn medium_wide() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 5);
}
#[test]
fn medium_tall() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 6);
}
#[test]
fn medium_square_four() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::Four), 16);
}
#[test]
fn medium_wide_four() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::Four), 20);
}
#[test]
fn medium_tall_four() {
assert_eq!(cents(FalGptImage1p5Quality::Medium, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::Four), 24);
}
// ── High quality (14¢ square, 20¢ wide/tall) ─────────────────────────
#[test]
fn high_square() {
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::One), 14);
}
#[test]
fn high_unset() {
assert_eq!(cents(FalGptImage1p5Quality::High, None, FalGptImage1p5NumImages::One), 14);
}
#[test]
fn high_wide() {
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::One), 20);
}
#[test]
fn high_tall() {
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Tall), FalGptImage1p5NumImages::One), 20);
}
#[test]
fn high_square_four() {
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Square), FalGptImage1p5NumImages::Four), 56);
}
#[test]
fn high_wide_four() {
assert_eq!(cents(FalGptImage1p5Quality::High, Some(FalGptImage1p5ImageSize::Wide), FalGptImage1p5NumImages::Four), 80);
}
// ── Metadata flags ────────────────────────────────────────────────────
#[test]
fn metadata_flags_are_default() {
let estimate = estimate_image_cost_fal_gpt_image_1p5(&plan(
FalGptImage1p5Quality::Medium, None, FalGptImage1p5NumImages::One,
));
assert!(!estimate.is_free);
assert!(!estimate.is_unlimited);
assert!(!estimate.is_rate_limited);
assert!(!estimate.has_watermark);
}
}
@@ -1,210 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::{
FalGptImage2ImageSize, FalGptImage2Quality, PlanFalGptImage2,
};
pub(crate) fn estimate_image_cost_fal_gpt_image_2(
plan: &PlanFalGptImage2,
) -> ImageGenerationCostEstimate {
// Per fal docs (fal-ai/gpt-image-2 and fal-ai/gpt-image-2/edit):
//
// Output image cost (per output image), rounded up to whole cents:
// Low: all sizes → 1¢
// Medium: landscape_4_3/portrait_4_3 → 4¢
// landscape_16_9/portrait_16_9 → 4¢
// square → 6¢
// square_hd → 6¢
// High: landscape_4_3/portrait_4_3 → 15¢
// landscape_16_9/portrait_16_9 → 16¢
// square → 22¢
// square_hd → 23¢
//
use FalGptImage2ImageSize as S;
let cost_per_image: u64 = match (plan.quality, plan.image_size) {
// Low quality
(FalGptImage2Quality::Low, _) => 1,
// Medium quality
(FalGptImage2Quality::Medium, None | Some(S::Square) | Some(S::SquareHd)) => 6,
(FalGptImage2Quality::Medium, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 4,
(FalGptImage2Quality::Medium, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 4,
(FalGptImage2Quality::Medium, Some(S::Auto)) => 6, // TODO: Not checked
// High quality
(FalGptImage2Quality::High, None | Some(S::Square)) => 22,
(FalGptImage2Quality::High, Some(S::SquareHd)) => 23,
(FalGptImage2Quality::High, Some(S::Landscape4x3) | Some(S::Portrait4x3)) => 15,
(FalGptImage2Quality::High, Some(S::Landscape16x9) | Some(S::Portrait16x9)) => 16,
(FalGptImage2Quality::High, Some(S::Auto)) => 23, // TODO: Not checked
};
let cost_in_usd_cents = cost_per_image * plan.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::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::FalGptImage2NumImages;
fn plan(
quality: FalGptImage2Quality,
image_size: Option<FalGptImage2ImageSize>,
num_images: FalGptImage2NumImages,
) -> PlanFalGptImage2 {
PlanFalGptImage2 {
prompt: Some("p".to_string()),
image_urls: vec![],
image_size,
quality,
num_images,
}
}
fn cents(
quality: FalGptImage2Quality,
image_size: Option<FalGptImage2ImageSize>,
num_images: FalGptImage2NumImages,
) -> u64 {
estimate_image_cost_fal_gpt_image_2(&plan(quality, image_size, num_images))
.cost_in_usd_cents.unwrap()
}
// -- Low quality (1c for all sizes) -------------------------------------------
#[test]
fn low_square() {
assert_eq!(cents(FalGptImage2Quality::Low, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::One), 1);
}
#[test]
fn low_square_hd() {
assert_eq!(cents(FalGptImage2Quality::Low, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::One), 1);
}
#[test]
fn low_unset() {
assert_eq!(cents(FalGptImage2Quality::Low, None, FalGptImage2NumImages::One), 1);
}
#[test]
fn low_landscape_4x3() {
assert_eq!(cents(FalGptImage2Quality::Low, Some(FalGptImage2ImageSize::Landscape4x3), FalGptImage2NumImages::One), 1);
}
#[test]
fn low_four() {
assert_eq!(cents(FalGptImage2Quality::Low, None, FalGptImage2NumImages::Four), 4);
}
// -- Medium quality -----------------------------------------------------------
#[test]
fn medium_square() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::One), 6);
}
#[test]
fn medium_square_hd() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::One), 6);
}
#[test]
fn medium_unset() {
assert_eq!(cents(FalGptImage2Quality::Medium, None, FalGptImage2NumImages::One), 6);
}
#[test]
fn medium_landscape_4x3() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Landscape4x3), FalGptImage2NumImages::One), 4);
}
#[test]
fn medium_portrait_4x3() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Portrait4x3), FalGptImage2NumImages::One), 4);
}
#[test]
fn medium_landscape_16x9() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Landscape16x9), FalGptImage2NumImages::One), 4);
}
#[test]
fn medium_portrait_16x9() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Portrait16x9), FalGptImage2NumImages::One), 4);
}
#[test]
fn medium_square_four() {
assert_eq!(cents(FalGptImage2Quality::Medium, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::Four), 24);
}
// -- High quality -------------------------------------------------------------
#[test]
fn high_square() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::One), 22);
}
#[test]
fn high_square_hd() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::One), 23);
}
#[test]
fn high_unset() {
assert_eq!(cents(FalGptImage2Quality::High, None, FalGptImage2NumImages::One), 22);
}
#[test]
fn high_landscape_4x3() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Landscape4x3), FalGptImage2NumImages::One), 15);
}
#[test]
fn high_portrait_4x3() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Portrait4x3), FalGptImage2NumImages::One), 15);
}
#[test]
fn high_landscape_16x9() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Landscape16x9), FalGptImage2NumImages::One), 16);
}
#[test]
fn high_portrait_16x9() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Portrait16x9), FalGptImage2NumImages::One), 16);
}
#[test]
fn high_square_four() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::Square), FalGptImage2NumImages::Four), 88);
}
#[test]
fn high_square_hd_four() {
assert_eq!(cents(FalGptImage2Quality::High, Some(FalGptImage2ImageSize::SquareHd), FalGptImage2NumImages::Four), 92);
}
// -- Metadata flags -----------------------------------------------------------
#[test]
fn metadata_flags_are_default() {
let estimate = estimate_image_cost_fal_gpt_image_2(&plan(
FalGptImage2Quality::Medium, None, FalGptImage2NumImages::One,
));
assert!(!estimate.is_free);
assert!(!estimate.is_unlimited);
assert!(!estimate.is_rate_limited);
assert!(!estimate.has_watermark);
}
}
@@ -1,35 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::{
FalNbNumImages, PlanFalNanoBanana,
};
pub(crate) fn estimate_image_cost_fal_nano_banana(
plan: &PlanFalNanoBanana,
) -> ImageGenerationCostEstimate {
// Pricing: $0.039/image (no resolution multiplier — Gemini 25 Flash has no resolution option).
// Rounded to 4 cents per image.
//
// Fal docs:
// Your request will cost $0.039 per image. For $1.00, you can run this model 25 times.
//
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
FalNbNumImages::One => 1,
FalNbNumImages::Two => 2,
FalNbNumImages::Three => 3,
FalNbNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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,
}
}
@@ -1,45 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::{
FalNb2NumImages, FalNb2Resolution, PlanFalNanaBanana2,
};
pub(crate) fn estimate_image_cost_fal_nano_banana_2(
plan: &PlanFalNanaBanana2,
) -> ImageGenerationCostEstimate {
// Pricing: $0.08/image base (1K). Multipliers: 0.5K = 0.75x, 2K = 1.5x, 4K = 2x.
// (Stored as USD cents.)
//
// Fal docs:
// Your request will cost $0.08 per image. For $1.00, you can run this model 12 times.
// 2K and 4K outputs will be charged at 1.5 times and 2 times the standard rate, respectively.
// 0.5K (512px) resolution outputs will be charged at 0.75 times the standard rate. If web
// search is used, an additional $0.015 will be charged. If high thinking is used,
// an additional $0.002 will be charged. Note: Pricing is subject to change.
//
let cost_per_image: u64 = match plan.resolution {
Some(FalNb2Resolution::HalfK) => 6, // $0.06 = $0.08 * 0.75
Some(FalNb2Resolution::OneK) => 8, // $0.08 base
Some(FalNb2Resolution::TwoK) => 12, // $0.12 = $0.08 * 1.5
Some(FalNb2Resolution::FourK) => 16, // $0.16 = $0.08 * 2.0
None => 8, // default = 1K base
};
let num_images: u64 = match plan.num_images {
FalNb2NumImages::One => 1,
FalNb2NumImages::Two => 2,
FalNb2NumImages::Three => 3,
FalNb2NumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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,
}
}
@@ -1,83 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::{
FalNbpNumImages, FalNbpResolution, PlanFalNanaBananaPro,
};
pub(crate) fn estimate_image_cost_fal_nano_banana_pro(
plan: &PlanFalNanaBananaPro,
) -> ImageGenerationCostEstimate {
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
// (Stored as USD cents.)
let cost_per_image: u64 = match plan.resolution {
Some(FalNbpResolution::FourK) => 30,
_ => 15,
};
let num_images: u64 = match plan.num_images {
FalNbpNumImages::One => 1,
FalNbpNumImages::Two => 2,
FalNbpNumImages::Three => 3,
FalNbpNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * 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 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 estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequestBuilder {
model: CommonImageModel::NanoBananaPro,
provider: Provider::Fal,
prompt: None,
image_inputs: None,
resolution,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// 1K/2K: $0.15/image = 15 cents
assert_eq!(estimate_usd_cents(None, 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 45);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
// 4K: $0.30/image = 30 cents
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 120);
}
}
@@ -1,20 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
pub(crate) fn estimate_image_cost_fal_seedream_4(
plan: &PlanFalSeedream4,
) -> ImageGenerationCostEstimate {
// Pricing: $0.03 per image.
let cost_per_image: u64 = 3;
let cost_in_usd_cents = cost_per_image * plan.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,
}
}
@@ -1,20 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
pub(crate) fn estimate_image_cost_fal_seedream_4p5(
plan: &PlanFalSeedream4p5,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04 per image.
let cost_per_image: u64 = 4;
let cost_in_usd_cents = cost_per_image * plan.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,
}
}
@@ -1,20 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
pub(crate) fn estimate_image_cost_fal_seedream_5_lite(
plan: &PlanFalSeedream5Lite,
) -> ImageGenerationCostEstimate {
// Pricing: $0.035 per image — round up to 4 cents.
let cost_per_image: u64 = 4;
let cost_in_usd_cents = cost_per_image * plan.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,
}
}
@@ -1,13 +0,0 @@
pub(crate) mod estimate_image_cost_fal_flux_1_dev;
pub(crate) mod estimate_image_cost_fal_flux_1_schnell;
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1;
pub(crate) mod estimate_image_cost_fal_flux_pro_1p1_ultra;
pub(crate) mod estimate_image_cost_fal_gpt_image_1;
pub(crate) mod estimate_image_cost_fal_gpt_image_1p5;
pub(crate) mod estimate_image_cost_fal_gpt_image_2;
pub(crate) mod estimate_image_cost_fal_seedream_4;
pub(crate) mod estimate_image_cost_fal_seedream_4p5;
pub(crate) mod estimate_image_cost_fal_seedream_5_lite;
pub(crate) mod estimate_image_cost_fal_nano_banana;
pub(crate) mod estimate_image_cost_fal_nano_banana_2;
pub(crate) mod estimate_image_cost_fal_nano_banana_pro;
@@ -1,2 +0,0 @@
pub(crate) mod artcraft;
pub(crate) mod fal;
@@ -1,119 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
pub async fn execute_artcraft_flux_1_dev(
plan: &PlanArtcraftFlux1Dev,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_1_dev_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_1_dev_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_dev() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_flux_1_dev_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_dev_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic".to_string()),
..base_flux_1_dev_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_1_dev_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a bear walking through an abandoned desert city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_1_dev_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,118 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
pub async fn execute_artcraft_flux_1_schnell(
plan: &PlanArtcraftFlux1Schnell,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_1_schnell_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::test_helpers::{base_flux_1_schnell_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_schnell() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a man and woman walking through a cyberpunk city at night".to_string()),
..base_flux_1_schnell_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_schnell_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic".to_string()),
..base_flux_1_schnell_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_1_schnell_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a man and woman walking through a futuristic city at evening".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_1_schnell_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,73 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::Flux2LoraEditImageAngleRequest;
use artcraft_client::endpoints::generate::image::angle::flux_2_lora_edit_image_angle::flux_2_lora_edit_image_angle;
pub async fn execute_artcraft_flux_2_lora_angles(
plan: &PlanArtcraftFlux2LoraAngles,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = Flux2LoraEditImageAngleRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
image_media_token: plan.image_input.clone(),
horizontal_angle: plan.horizontal_angle,
vertical_angle: plan.vertical_angle,
zoom: plan.zoom,
num_images: Some(plan.num_images),
image_size: plan.image_size,
};
let response = flux_2_lora_edit_image_angle(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_2_lora_angles_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_angle_edit_flux_2_lora() {
let client = get_artcraft_client();
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
quality: None,
image_batch_count: Some(1),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
horizontal_angle: Some(30.0),
vertical_angle: Some(0.0),
zoom: Some(1.0),
..base_flux_2_lora_angles_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,119 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
pub async fn execute_artcraft_flux_pro_1p1(
plan: &PlanArtcraftFluxPro11,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_pro_11_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_pro_1p1_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_flux_pro_1p1_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic".to_string()),
..base_flux_pro_1p1_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_pro_1p1_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a bear walking through an abandoned desert city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_pro_1p1_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,119 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
pub async fn execute_artcraft_flux_pro_1p1_ultra(
plan: &PlanArtcraftFluxPro11Ultra,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_pro_11_ultra_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_pro_1p1_ultra_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_ultra() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_flux_pro_1p1_ultra_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_ultra_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic".to_string()),
..base_flux_pro_1p1_ultra_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_pro_1p1_ultra_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a t-rex walking through an abandoned desert city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_pro_1p1_ultra_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,147 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::PlanArtcraftGptImage1;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::GptImage1EditImageRequest;
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::GenerateGptImage1TextToImageRequest;
use artcraft_client::endpoints::generate::image::edit::gpt_image_1_edit_image::gpt_image_1_edit_image;
use artcraft_client::endpoints::generate::image::text::generate_gpt_image_1_text_to_image::generate_gpt_image_1_text_to_image;
/// GPT Image 1 has two distinct legacy storyteller-web endpoints (one for
/// text-to-image, one for image-edit), so we dispatch on whether image refs
/// are present rather than calling a single multi-function endpoint.
pub async fn execute_artcraft_gpt_image_1(
plan: &PlanArtcraftGptImage1,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let inference_job_token = match plan.image_inputs.clone() {
Some(image_inputs) => {
let request = GptImage1EditImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: Some(image_inputs.to_owned()),
image_size: plan.image_size.map(|s| s.to_edit()),
num_images: Some(plan.num_images.to_edit()),
image_quality: Some(plan.quality.to_edit()),
};
let response = gpt_image_1_edit_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
response.inference_job_token
}
None => {
let request = GenerateGptImage1TextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_size: plan.image_size.map(|s| s.to_t2i()),
num_images: Some(plan.num_images.to_t2i()),
image_quality: Some(plan.quality.to_t2i()),
};
let response = generate_gpt_image_1_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
response.inference_job_token
}
};
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_gpt_image_1_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
// Build-only smoke test that exercises the full text-to-image plan path
// (no network I/O — we never call generate_image).
#[test]
fn build_text_to_image_plan_smoke() {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_gpt_image_1_image_request()
};
let plan = request.build().expect("plan should build");
assert!(matches!(plan, ImageGenerationPlan::ArtcraftGptImage1(_)));
}
// Build-only smoke test for the edit path. We can't actually fire the
// request without an authenticated client, but we want to make sure the
// dispatcher recognizes media-token inputs as edit-mode.
#[test]
fn build_edit_image_plan_smoke() {
let tokens = vec![MediaFileToken::new_from_str("mf_test00000000000000000000000000")];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_gpt_image_1_image_request()
};
let plan = request.build().expect("plan should build");
let ImageGenerationPlan::ArtcraftGptImage1(plan) = plan else {
panic!("expected ArtcraftGptImage1")
};
assert!(plan.image_inputs.is_some(), "edit mode must carry image_inputs");
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_1() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
..base_gpt_image_1_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_gpt_image_1() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a desert with cacti".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_gpt_image_1_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
}
}
@@ -1,164 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::GptImage1p5MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen;
pub async fn execute_artcraft_gpt_image_1p5(
plan: &PlanArtcraftGptImage1p5,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GptImage1p5MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
mask_image_token: None,
num_images: Some(plan.num_images),
image_size: plan.image_size,
background: None,
quality: Some(plan.quality),
input_fidelity: None,
output_format: None,
};
let response = gpt_image_1p5_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_gpt_image_1p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_1p5() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
..base_gpt_image_1p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_gpt_image_1p5() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a desert with cacti".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto),
image_batch_count: Some(1),
..base_gpt_image_1p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_gpt_image_1p5_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a bear walking through a abandoned desert city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_gpt_image_1p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_gpt_image_1p5_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to an alien world".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_gpt_image_1p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,145 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::PlanArtcraftGptImage2;
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 enums::common::generation::common_image_model::CommonImageModel;
/// Execute GPT Image 2 via the Artcraft omni-gen image endpoint.
pub async fn execute_artcraft_gpt_image_2(
plan: &PlanArtcraftGptImage2,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = OmniGenImageCostAndGenerateRequest {
idempotency_token: Some(plan.idempotency_token.clone()),
model: Some(CommonImageModel::GptImage2),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
resolution: None,
aspect_ratio: plan.aspect_ratio_enum(),
quality: Some(plan.quality_enum()),
image_batch_count: Some(plan.num_images_u16()),
adjust_horizontal_angle: None,
adjust_vertical_angle: None,
adjust_zoom: None,
};
let response = omni_gen_image_generate(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use artcraft_client::utils::api_host::ApiHost;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_quality::CommonQuality;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{get_artcraft_client, get_artcraft_client_for_env};
use tokens::tokens::media_files::MediaFileToken;
fn base_gpt_image_2_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: crate::api::common_image_model::CommonImageModel::GptImage2,
provider: crate::api::provider::Provider::Artcraft,
prompt: Some("a t-rex walking through a cyberpunk city at night".to_string()),
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
}
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_2() {
let client = get_artcraft_client_for_env(ApiHost::Localhost { port: 12345 });
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
..base_gpt_image_2_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_gpt_image_2() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a desert with cacti".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
image_batch_count: Some(1),
..base_gpt_image_2_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_2_high_quality() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: Some(CommonQuality::High),
image_batch_count: Some(1),
..base_gpt_image_2_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,163 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::NanoBananaMultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_image::nano_banana_multi_function_image_gen;
pub async fn execute_artcraft_nano_banana(
plan: &PlanArtcraftNanaBanana,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = NanoBananaMultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
num_images: Some(plan.num_images),
aspect_ratio: plan.aspect_ratio,
};
let response = nano_banana_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_nano_banana_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_nano_banana_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_nano_banana() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto),
quality: None,
image_batch_count: Some(1),
..base_nano_banana_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_nano_banana_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_nano_banana_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_nano_banana_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_nano_banana_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,68 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::NanaBanana2MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_2_multi_function_image_gen_image::nano_banana_2_multi_function_image_gen;
pub async fn execute_artcraft_nano_banana_2(
plan: &PlanArtcraftNanaBanana2,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = NanaBanana2MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
num_images: Some(plan.num_images),
resolution: plan.resolution,
aspect_ratio: plan.aspect_ratio,
};
let response = nano_banana_2_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_nano_banana_2_image_request, get_artcraft_client};
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_2() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_nano_banana_2_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,126 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen;
pub async fn execute_artcraft_nano_banana_pro(
plan: &PlanArtcraftNanaBananaPro,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
num_images: Some(plan.num_images),
resolution: plan.resolution,
aspect_ratio: plan.aspect_ratio,
};
let response = nano_banana_pro_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_pro() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_pro_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic".to_string()),
..base_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_nano_banana_pro() {
let client = get_artcraft_client();
// Reference images: the man to transplant, then the destination scene.
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
MediaFileToken::new_from_str("m_2220d5we3masng5hvzg5zz68ywbdqd"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Add the man in the first image into the fantasy scene in the second image. Keep his hoodie and attire, but match the vibe of the fantasy scene.".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(1),
..base_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,75 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
use artcraft_api_defs::generate::image::angle::qwen_edit_2511_edit_image_angle::QwenEdit2511EditImageAngleRequest;
use artcraft_client::endpoints::generate::image::angle::qwen_edit_2511_edit_image_angle::qwen_edit_2511_edit_image_angle;
pub async fn execute_artcraft_qwen_edit_2511_angles(
plan: &PlanArtcraftQwenEdit2511Angles,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = QwenEdit2511EditImageAngleRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
image_media_token: plan.image_input.clone(),
horizontal_angle: plan.horizontal_angle,
vertical_angle: plan.vertical_angle,
zoom: plan.zoom,
additional_prompt: plan.prompt.clone(),
num_images: Some(plan.num_images),
image_size: plan.image_size,
};
let response = qwen_edit_2511_edit_image_angle(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_qwen_edit_2511_angles_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_angle_edit_qwen_edit_2511() {
let client = get_artcraft_client();
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
quality: None,
image_batch_count: Some(1),
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
horizontal_angle: Some(30.0),
vertical_angle: Some(0.0),
zoom: Some(1.0),
..base_qwen_edit_2511_angles_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,164 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::BytedanceSeedreamV4MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
pub async fn execute_artcraft_seedream_4(
plan: &PlanArtcraftSeedream4,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
num_images: Some(plan.num_images),
image_size: plan.image_size,
max_images: None,
};
let response = bytedance_seedream_v4_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night".to_string()),
..base_seedream_4_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_seedream_4() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto),
quality: None,
image_batch_count: Some(1),
..base_seedream_4_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_seedream_4_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_seedream_4_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a sunny beach".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,164 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::BytedanceSeedreamV4p5MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
pub async fn execute_artcraft_seedream_4p5(
plan: &PlanArtcraftSeedream4p5,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
num_images: Some(plan.num_images),
image_size: plan.image_size,
max_images: None,
};
let response = bytedance_seedream_v4p5_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4p5() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
..base_seedream_4p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_seedream_4p5() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a spooky haunted house".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto2k),
quality: None,
image_batch_count: Some(1),
..base_seedream_4p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_seedream_4p5_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_seedream_4p5_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequestBuilder {
prompt: Some("Change the background to a spooky haunted house".to_string()),
image_inputs: Some(ImageListRef::MediaFileTokens(image_tokens.clone())),
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,66 +0,0 @@
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,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::BytedanceSeedream5LiteMultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen_image::bytedance_seedream_5_lite_multi_function_image_gen;
pub async fn execute_artcraft_seedream_5_lite(
plan: &PlanArtcraftSeedream5Lite,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedream5LiteMultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.clone(),
image_media_tokens: plan.image_inputs.clone(),
num_images: Some(plan.num_images),
image_size: plan.image_size,
max_images: None,
};
let response = bytedance_seedream_5_lite_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_5_lite_image_request, get_artcraft_client};
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_5_lite() {
let client = get_artcraft_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night".to_string()),
..base_seedream_5_lite_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,15 +0,0 @@
pub mod generate_image_artcraft_flux_1_dev;
pub mod generate_image_artcraft_flux_1_schnell;
pub mod generate_image_artcraft_flux_pro_1p1;
pub mod generate_image_artcraft_flux_pro_1p1_ultra;
pub mod generate_image_artcraft_gpt_image_1;
pub mod generate_image_artcraft_gpt_image_1p5;
pub mod generate_image_artcraft_gpt_image_2;
pub mod generate_image_artcraft_nano_banana;
pub mod generate_image_artcraft_nano_banana_2;
pub mod generate_image_artcraft_nano_banana_pro;
pub mod generate_image_artcraft_seedream_4;
pub mod generate_image_artcraft_seedream_4p5;
pub mod generate_image_artcraft_seedream_5_lite;
pub mod generate_image_artcraft_qwen_edit_2511_angles;
pub mod generate_image_artcraft_flux_2_lora_angles;
@@ -1,65 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
use fal_client::requests::webhook::image::edit::enqueue_flux_1_dev_edit_image_webhook::{
enqueue_flux_1_dev_edit_image_webhook, Flux1DevEditImageArgs, Flux1DevEditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_flux_1_dev_text_to_image_webhook::{
enqueue_flux_1_dev_text_to_image_webhook, Flux1DevArgs, Flux1DevRequest,
};
pub async fn execute_fal_flux_1_dev(
plan: &PlanFalFlux1Dev,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if let Some(image_url) = &plan.maybe_image_url {
// Image-to-image mode
let request = Flux1DevEditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_url: image_url.clone(),
num_images: plan.num_images.to_edit(),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1DevEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_flux_1_dev_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Text-to-image mode
let request = Flux1DevRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_t2i(),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1DevArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_flux_1_dev_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
@@ -1,65 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
use fal_client::requests::webhook::image::edit::enqueue_flux_1_schnell_edit_image_webhook::{
enqueue_flux_1_schnell_edit_image_webhook, Flux1SchnellEditImageArgs, Flux1SchnellEditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_flux_1_schnell_text_to_image_webhook::{
enqueue_flux_1_schnell_text_to_image_webhook, Flux1SchnellArgs, Flux1SchnellRequest,
};
pub async fn execute_fal_flux_1_schnell(
plan: &PlanFalFlux1Schnell,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if let Some(image_url) = &plan.maybe_image_url {
// Image-to-image (redux) mode
let request = Flux1SchnellEditImageRequest {
image_url: image_url.clone(),
num_images: plan.num_images.to_edit(),
image_size: plan.edit_image_size,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1SchnellEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_flux_1_schnell_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Text-to-image mode
let request = Flux1SchnellRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.t2i_aspect_ratio,
num_images: plan.num_images.to_t2i(),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Flux1SchnellArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_flux_1_schnell_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
@@ -1,42 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
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,
};
pub async fn execute_fal_flux_pro_1p1(
plan: &PlanFalFluxPro11,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = FluxPro11Request {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_fal(),
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = FluxPro11Args {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_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_request),
}))
}
@@ -1,42 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
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,
};
pub async fn execute_fal_flux_pro_1p1_ultra(
plan: &PlanFalFluxPro11Ultra,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = FluxPro11UltraRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_fal(),
};
let outbound_request: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = FluxPro11UltraArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_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_request),
}))
}
@@ -1,178 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::PlanFalGptImage1;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{
enqueue_gpt_image_1_edit_image_webhook, EnqueueGptImage1EditImageArgs,
EnqueueGptImage1EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
enqueue_gpt_image_1_text_to_image_webhook, EnqueueGptImage1TextToImageArgs,
EnqueueGptImage1TextToImageRequest,
};
pub async fn execute_fal_gpt_image_1(
plan: &PlanFalGptImage1,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueGptImage1TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: plan.num_images.to_t2i(),
image_size: plan.image_size.map(|s| s.to_t2i()),
quality: Some(plan.quality.to_t2i()),
background: None,
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gpt_image_1_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let request = EnqueueGptImage1EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: plan.num_images.to_edit(),
mask_image_url: None,
image_size: plan.image_size.map(|s| s.to_edit()),
quality: Some(plan.quality.to_edit()),
input_fidelity: None,
background: None,
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gpt_image_1_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
#[cfg(test)]
mod tests {
use test_data::web::image_urls::TREX_SKELETON_IMAGE_URL;
use crate::api::common_aspect_ratio::CommonAspectRatio;
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 crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::get_fal_client;
fn base_fal_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage1,
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,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
}
}
// Build-only smoke test (no network I/O).
#[test]
fn build_text_to_image_plan_smoke() {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_fal_request()
};
let plan = request.build().expect("plan should build");
let ImageGenerationPlan::FalGptImage1(plan) = plan else {
panic!("expected FalGptImage1")
};
assert!(plan.image_urls.is_empty(), "text mode must have no image_urls");
}
// Build-only smoke test for edit mode.
#[test]
fn build_edit_image_plan_smoke() {
let urls = vec!["https://example.com/img.jpg".to_string()];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_fal_request()
};
let plan = request.build().expect("plan should build");
let ImageGenerationPlan::FalGptImage1(plan) = plan else {
panic!("expected FalGptImage1")
};
assert_eq!(plan.image_urls, urls);
}
#[tokio::test]
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn test_text_to_image_gpt_image_1_fal() {
let client = get_fal_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
..base_fal_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request id: {:?}", payload.request_id);
}
#[tokio::test]
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn test_edit_image_gpt_image_1_fal() {
let client = get_fal_client();
let urls = vec![
TREX_SKELETON_IMAGE_URL.to_string(),
];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
prompt: Some("change the background to a desert".to_string()),
..base_fal_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request id: {:?}", payload.request_id);
}
}
@@ -1,74 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
enqueue_gpt_image_1p5_image_edit_webhook, EnqueueGptImage1p5EditImageArgs,
EnqueueGptImage1p5EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs,
EnqueueGptImage1p5TextToImageRequest,
};
pub async fn execute_fal_gpt_image_1p5(
plan: &PlanFalGptImage1p5,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueGptImage1p5TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: plan.num_images.to_t2i(),
image_size: plan.image_size.map(|s| s.to_t2i()),
background: None,
quality: Some(plan.quality.to_t2i()),
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1p5TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gpt_image_1p5_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let request = EnqueueGptImage1p5EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: plan.num_images.to_edit(),
mask_image_url: None,
image_size: plan.image_size.map(|s| s.to_edit()),
background: None,
quality: Some(plan.quality.to_edit()),
input_fidelity: None,
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage1p5EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gpt_image_1p5_image_edit_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
@@ -1,193 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::PlanFalGptImage2;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_2_edit_image_webhook::{
enqueue_gpt_image_2_edit_image_webhook, EnqueueGptImage2EditImageArgs,
EnqueueGptImage2EditImageRequest,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_2_text_to_image_webhook::{
enqueue_gpt_image_2_text_to_image_webhook, EnqueueGptImage2TextToImageArgs,
EnqueueGptImage2TextToImageRequest,
};
pub async fn execute_fal_gpt_image_2(
plan: &PlanFalGptImage2,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueGptImage2TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: plan.num_images.to_t2i(),
image_size: plan.image_size.map(|s| s.to_t2i()),
quality: Some(plan.quality.to_t2i()),
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage2TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gpt_image_2_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let request = EnqueueGptImage2EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: plan.num_images.to_edit(),
mask_url: None,
image_size: plan.image_size.map(|s| s.to_edit()),
quality: Some(plan.quality.to_edit()),
output_format: None,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueGptImage2EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gpt_image_2_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
#[cfg(test)]
mod tests {
use test_data::web::image_urls::{GHOST_IMAGE_URL, JUNO_AT_LAKE_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
use crate::api::common_aspect_ratio::CommonAspectRatio;
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 crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::get_fal_client;
// ── Build smoke tests ──
mod build_smoke_tests {
use super::*;
#[test]
fn text_to_image_plan() {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_request()
};
let plan = request.build().expect("plan should build");
let ImageGenerationPlan::FalGptImage2(plan) = plan else {
panic!("expected FalGptImage2")
};
assert!(plan.image_urls.is_empty(), "text mode must have no image_urls");
}
#[test]
fn edit_image_plan() {
let urls = vec![
JUNO_AT_LAKE_IMAGE_URL.to_string(),
GHOST_IMAGE_URL.to_string(),
];
let request = GenerateImageRequestBuilder {
prompt: Some("dog running from a scary ghost".to_string()),
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_request()
};
let plan = request.build().expect("plan should build");
let ImageGenerationPlan::FalGptImage2(plan) = plan else {
panic!("expected FalGptImage2")
};
assert_eq!(plan.image_urls, urls);
}
}
// ── Live API tests ──
mod live_api_tests {
use super::*;
#[tokio::test]
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn text_to_image() {
let client = get_fal_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night".to_string()),
..base_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request id: {:?}", payload.request_id);
assert_eq!(1, 2, "Inspect output above");
}
#[tokio::test]
#[ignore] // manually run — fires a real fal API request and incurs cost
async fn edit_image() {
let client = get_fal_client();
let request = GenerateImageRequestBuilder {
prompt: Some("dog running from a scary ghost".to_string()),
image_inputs: Some(ImageListRef::Urls(vec![
JUNO_AT_LAKE_IMAGE_URL.to_string(),
GHOST_IMAGE_URL.to_string(),
])),
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
..base_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request id: {:?}", payload.request_id);
assert_eq!(1, 2, "Inspect output above");
}
}
// ── Helpers ──
fn base_request() -> GenerateImageRequestBuilder {
GenerateImageRequestBuilder {
model: CommonImageModel::GptImage2,
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,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
horizontal_angle: None,
vertical_angle: None,
zoom: None,
}
}
}
@@ -1,88 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::{
FalNbNumImages, PlanFalNanoBanana,
};
use fal_client::requests::webhook::image::edit::enqueue_gemini_25_flash_edit_webhook::{
enqueue_gemini_25_flash_edit_webhook, Gemini25FlashEditArgs,
Gemini25FlashEditNumImages, 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,
Gemini25FlashTextToImageNumImages, Gemini25FlashTextToImageRequest,
};
pub async fn execute_fal_nano_banana(
plan: &PlanFalNanoBanana,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
// Text-to-image mode
let request = Gemini25FlashTextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: to_t2i_num_images(plan.num_images),
aspect_ratio: plan.t2i_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Gemini25FlashTextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gemini_25_flash_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Image-edit mode
let request = Gemini25FlashEditRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
aspect_ratio: plan.edit_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = Gemini25FlashEditArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_gemini_25_flash_edit_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
fn to_t2i_num_images(n: FalNbNumImages) -> Gemini25FlashTextToImageNumImages {
match n {
FalNbNumImages::One => Gemini25FlashTextToImageNumImages::One,
FalNbNumImages::Two => Gemini25FlashTextToImageNumImages::Two,
FalNbNumImages::Three => Gemini25FlashTextToImageNumImages::Three,
FalNbNumImages::Four => Gemini25FlashTextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: FalNbNumImages) -> Gemini25FlashEditNumImages {
match n {
FalNbNumImages::One => Gemini25FlashEditNumImages::One,
FalNbNumImages::Two => Gemini25FlashEditNumImages::Two,
FalNbNumImages::Three => Gemini25FlashEditNumImages::Three,
FalNbNumImages::Four => Gemini25FlashEditNumImages::Four,
}
}
@@ -1,110 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::{
FalNb2NumImages, FalNb2Resolution, PlanFalNanaBanana2,
};
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_2_edit_image_webhook::{
enqueue_nano_banana_2_edit_image_webhook, EnqueueNanoBanana2EditImageArgs,
EnqueueNanoBanana2EditImageNumImages, EnqueueNanoBanana2EditImageRequest,
EnqueueNanoBanana2EditImageResolution,
};
use fal_client::requests::webhook::image::text::enqueue_nano_banana_2_text_to_image_webhook::{
enqueue_nano_banana_2_text_to_image_webhook, EnqueueNanoBanana2TextToImageArgs,
EnqueueNanoBanana2TextToImageNumImages, EnqueueNanoBanana2TextToImageRequest,
EnqueueNanoBanana2TextToImageResolution,
};
pub async fn execute_fal_nano_banana_2(
plan: &PlanFalNanaBanana2,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
// Text-to-image mode
let request = EnqueueNanoBanana2TextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: to_t2i_num_images(plan.num_images),
resolution: plan.resolution.map(to_t2i_resolution),
aspect_ratio: plan.t2i_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBanana2TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_nano_banana_2_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Image-edit mode
let request = EnqueueNanoBanana2EditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
resolution: plan.resolution.map(to_edit_resolution),
aspect_ratio: plan.edit_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBanana2EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_nano_banana_2_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
fn to_t2i_num_images(n: FalNb2NumImages) -> EnqueueNanoBanana2TextToImageNumImages {
match n {
FalNb2NumImages::One => EnqueueNanoBanana2TextToImageNumImages::One,
FalNb2NumImages::Two => EnqueueNanoBanana2TextToImageNumImages::Two,
FalNb2NumImages::Three => EnqueueNanoBanana2TextToImageNumImages::Three,
FalNb2NumImages::Four => EnqueueNanoBanana2TextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: FalNb2NumImages) -> EnqueueNanoBanana2EditImageNumImages {
match n {
FalNb2NumImages::One => EnqueueNanoBanana2EditImageNumImages::One,
FalNb2NumImages::Two => EnqueueNanoBanana2EditImageNumImages::Two,
FalNb2NumImages::Three => EnqueueNanoBanana2EditImageNumImages::Three,
FalNb2NumImages::Four => EnqueueNanoBanana2EditImageNumImages::Four,
}
}
fn to_t2i_resolution(r: FalNb2Resolution) -> EnqueueNanoBanana2TextToImageResolution {
match r {
FalNb2Resolution::HalfK => EnqueueNanoBanana2TextToImageResolution::HalfK,
FalNb2Resolution::OneK => EnqueueNanoBanana2TextToImageResolution::OneK,
FalNb2Resolution::TwoK => EnqueueNanoBanana2TextToImageResolution::TwoK,
FalNb2Resolution::FourK => EnqueueNanoBanana2TextToImageResolution::FourK,
}
}
fn to_edit_resolution(r: FalNb2Resolution) -> EnqueueNanoBanana2EditImageResolution {
match r {
FalNb2Resolution::HalfK => EnqueueNanoBanana2EditImageResolution::HalfK,
FalNb2Resolution::OneK => EnqueueNanoBanana2EditImageResolution::OneK,
FalNb2Resolution::TwoK => EnqueueNanoBanana2EditImageResolution::TwoK,
FalNb2Resolution::FourK => EnqueueNanoBanana2EditImageResolution::FourK,
}
}
@@ -1,206 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::{
FalNbpNumImages, FalNbpResolution, PlanFalNanaBananaPro,
};
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{
enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs,
EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageRequest,
EnqueueNanoBananaProEditImageResolution,
};
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{
enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs,
EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageRequest,
EnqueueNanoBananaProTextToImageResolution,
};
pub async fn execute_fal_nano_banana_pro(
plan: &PlanFalNanaBananaPro,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
// Text-to-image mode
let request = EnqueueNanoBananaProTextToImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
num_images: to_t2i_num_images(plan.num_images),
resolution: plan.resolution.map(to_t2i_resolution),
aspect_ratio: plan.t2i_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBananaProTextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_nano_banana_pro_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
// Image-edit mode
let request = EnqueueNanoBananaProEditImageRequest {
prompt: plan.prompt.as_deref().unwrap_or("").to_string(),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
resolution: plan.resolution.map(to_edit_resolution),
aspect_ratio: plan.edit_aspect_ratio,
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueNanoBananaProEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_nano_banana_pro_image_edit_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
fn to_t2i_num_images(n: FalNbpNumImages) -> EnqueueNanoBananaProTextToImageNumImages {
match n {
FalNbpNumImages::One => EnqueueNanoBananaProTextToImageNumImages::One,
FalNbpNumImages::Two => EnqueueNanoBananaProTextToImageNumImages::Two,
FalNbpNumImages::Three => EnqueueNanoBananaProTextToImageNumImages::Three,
FalNbpNumImages::Four => EnqueueNanoBananaProTextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: FalNbpNumImages) -> EnqueueNanoBananaProEditImageNumImages {
match n {
FalNbpNumImages::One => EnqueueNanoBananaProEditImageNumImages::One,
FalNbpNumImages::Two => EnqueueNanoBananaProEditImageNumImages::Two,
FalNbpNumImages::Three => EnqueueNanoBananaProEditImageNumImages::Three,
FalNbpNumImages::Four => EnqueueNanoBananaProEditImageNumImages::Four,
}
}
fn to_t2i_resolution(r: FalNbpResolution) -> EnqueueNanoBananaProTextToImageResolution {
match r {
FalNbpResolution::OneK => EnqueueNanoBananaProTextToImageResolution::OneK,
FalNbpResolution::TwoK => EnqueueNanoBananaProTextToImageResolution::TwoK,
FalNbpResolution::FourK => EnqueueNanoBananaProTextToImageResolution::FourK,
}
}
fn to_edit_resolution(r: FalNbpResolution) -> EnqueueNanoBananaProEditImageResolution {
match r {
FalNbpResolution::OneK => EnqueueNanoBananaProEditImageResolution::OneK,
FalNbpResolution::TwoK => EnqueueNanoBananaProEditImageResolution::TwoK,
FalNbpResolution::FourK => EnqueueNanoBananaProEditImageResolution::FourK,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_fal_image_request, get_fal_client};
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
#[tokio::test]
#[ignore] // manually run — fires a real Fal API request
async fn test_text_to_image_fal_nano_banana_pro() {
let client = get_fal_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::TwoK),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets".to_string()),
..base_fal_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request ID: {:?}", payload.request_id);
println!("Gateway request ID: {:?}", payload.gateway_request_id);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real Fal API request
async fn test_text_to_image_fal_nano_banana_pro_batch_four() {
let client = get_fal_client();
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(4),
prompt: Some("a golden retriever surfing a wave, cinematic, 4K".to_string()),
..base_fal_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request ID: {:?}", payload.request_id);
println!("Gateway request ID: {:?}", payload.gateway_request_id);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real Fal API request
async fn test_edit_image_fal_nano_banana_pro() {
let client = get_fal_client();
let image_urls = vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
];
let request = GenerateImageRequestBuilder {
prompt: Some("Add the ghost from the first image hovering above the T-Rex skeleton in the second image, make it look spooky but friendly".to_string()),
image_inputs: Some(ImageListRef::Urls(image_urls.clone())),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
resolution: Some(CommonResolution::TwoK),
quality: None,
image_batch_count: Some(1),
..base_fal_image_request()
};
let plan = request.build().unwrap();
let ImageGenerationPlan::FalNanaBananaPro(ref p) = plan else {
panic!("expected FalNanaBananaPro plan");
};
println!("Plan image_urls: {:?}", p.image_urls);
println!("Plan edit_aspect_ratio: {:?}", p.edit_aspect_ratio);
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request ID: {:?}", payload.request_id);
println!("Gateway request ID: {:?}", payload.gateway_request_id);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,68 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
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,
};
pub async fn execute_fal_seedream_4(
plan: &PlanFalSeedream4,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueBytedanceSeedreamV4TextToImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let request = EnqueueBytedanceSeedreamV4EditImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
image_urls: plan.image_urls.clone(),
num_images: Some(plan.num_images.to_edit()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_edit()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_bytedance_seedream_v4_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
@@ -1,68 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
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,
};
pub async fn execute_fal_seedream_4p5(
plan: &PlanFalSeedream4p5,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueBytedanceSeedreamV4p5TextToImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let request = EnqueueBytedanceSeedreamV4p5EditImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
image_urls: plan.image_urls.clone(),
num_images: Some(plan.num_images.to_edit()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_edit()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_bytedance_seedream_v4p5_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
@@ -1,68 +0,0 @@
use std::fmt::Debug;
use std::sync::Arc;
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,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
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,
};
pub async fn execute_fal_seedream_5_lite(
plan: &PlanFalSeedream5Lite,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let (webhook_response, outbound_request) = if plan.image_urls.is_empty() {
let request = EnqueueBytedanceSeedreamV5LiteTextToImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
} else {
let request = EnqueueBytedanceSeedreamV5LiteEditImageRequest {
prompt: plan.prompt.clone().unwrap_or_default(),
image_urls: plan.image_urls.clone(),
num_images: Some(plan.num_images.to_edit()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_edit()),
};
let outbound: Arc<dyn Debug + Send + Sync> = Arc::new(request.clone());
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
request,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
let resp = enqueue_bytedance_seedream_v5_lite_edit_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?;
(resp, outbound)
};
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_request),
}))
}
@@ -1,13 +0,0 @@
pub mod generate_image_fal_flux_1_dev;
pub mod generate_image_fal_flux_1_schnell;
pub mod generate_image_fal_flux_pro_1p1;
pub mod generate_image_fal_flux_pro_1p1_ultra;
pub mod generate_image_fal_gpt_image_1;
pub mod generate_image_fal_gpt_image_1p5;
pub mod generate_image_fal_gpt_image_2;
pub mod generate_image_fal_seedream_4;
pub mod generate_image_fal_seedream_4p5;
pub mod generate_image_fal_seedream_5_lite;
pub mod generate_image_fal_nano_banana;
pub mod generate_image_fal_nano_banana_2;
pub mod generate_image_fal_nano_banana_pro;
@@ -1,58 +1,57 @@
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::router_aspect_ratio::RouterAspectRatio;
use crate::api::router_image_model::RouterImageModel;
use crate::api::router_quality::RouterQuality;
use crate::api::router_resolution::RouterResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::api::router_provider::RouterProvider;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
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::image_generation_plan::ImageGenerationPlan;
use crate::generate::generate_image_v2::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
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::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_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::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;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::plan_generate_image_artcraft_flux_pro_1p1;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::plan_generate_image_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::plan_generate_image_artcraft_gpt_image_1;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::plan_generate_image_artcraft_gpt_image_1p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::plan_generate_image_artcraft_gpt_image_2;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::plan_generate_image_artcraft_nano_banana;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::plan_generate_image_artcraft_nano_banana_2;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::plan_generate_image_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::plan_generate_image_fal_flux_1_dev;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::plan_generate_image_fal_flux_1_schnell;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::plan_generate_image_fal_flux_pro_1p1;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::plan_generate_image_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::plan_generate_image_fal_gpt_image_1;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::plan_generate_image_fal_gpt_image_1p5;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::plan_generate_image_fal_gpt_image_2;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::plan_generate_image_fal_seedream_4;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::plan_generate_image_fal_seedream_4p5;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::plan_generate_image_fal_seedream_5_lite;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::plan_generate_image_fal_nano_banana;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::plan_generate_image_fal_nano_banana_2;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::build::build_artcraft_flux_1_dev;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::build::build_artcraft_flux_1_schnell;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::build::build_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::build::build_artcraft_flux_pro_1p1;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::build::build_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::build::build_artcraft_gpt_image_1;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::build::build_artcraft_gpt_image_1p5;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::build::build_artcraft_gpt_image_2;
use crate::generate::generate_image::providers::artcraft::midjourney_7::build::build_artcraft_midjourney_7;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::build::build_artcraft_midjourney_7_niji;
use crate::generate::generate_image::providers::artcraft::midjourney_8::build::build_artcraft_midjourney_8;
use crate::generate::generate_image::providers::artcraft::nano_banana::build::build_artcraft_nano_banana;
use crate::generate::generate_image::providers::artcraft::nano_banana_2::build::build_artcraft_nano_banana_2;
use crate::generate::generate_image::providers::artcraft::nano_banana_pro::build::build_artcraft_nano_banana_pro;
use crate::generate::generate_image::providers::artcraft::qwen_edit_2511_angles::build::build_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::providers::artcraft::seedream_4::build::build_artcraft_seedream_4;
use crate::generate::generate_image::providers::artcraft::seedream_4p5::build::build_artcraft_seedream_4p5;
use crate::generate::generate_image::providers::artcraft::seedream_5_lite::build::build_artcraft_seedream_5_lite;
use crate::generate::generate_image::providers::fal::flux_1_dev::build::build_fal_flux_1_dev;
use crate::generate::generate_image::providers::fal::flux_1_schnell::build::build_fal_flux_1_schnell;
use crate::generate::generate_image::providers::fal::flux_2_lora_angles::build::build_fal_flux_2_lora_angles;
use crate::generate::generate_image::providers::fal::flux_pro_1p1::build::build_fal_flux_pro_1p1;
use crate::generate::generate_image::providers::fal::flux_pro_1p1_ultra::build::build_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image::providers::fal::gpt_image_1::build::build_fal_gpt_image_1;
use crate::generate::generate_image::providers::fal::gpt_image_1p5::build::build_fal_gpt_image_1p5;
use crate::generate::generate_image::providers::fal::gpt_image_2::build::build_fal_gpt_image_2;
use crate::generate::generate_image::providers::fal::nano_banana::build::build_fal_nano_banana;
use crate::generate::generate_image::providers::fal::nano_banana_2::build::build_fal_nano_banana_2;
use crate::generate::generate_image::providers::fal::nano_banana_pro::build::build_fal_nano_banana_pro;
use crate::generate::generate_image::providers::fal::qwen_edit_2511_angles::build::build_fal_qwen_edit_2511_angles;
use crate::generate::generate_image::providers::fal::seedream_4::build::build_fal_seedream_4;
use crate::generate::generate_image::providers::fal::seedream_4p5::build::build_fal_seedream_4p5;
use crate::generate::generate_image::providers::fal::seedream_5_lite::build::build_fal_seedream_5_lite;
use crate::generate::generate_image::providers::kinovi::midjourney_7::build::build_kinovi_midjourney_7;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::build::build_kinovi_midjourney_7_niji;
use crate::generate::generate_image::providers::kinovi::midjourney_8::build::build_kinovi_midjourney_8;
#[derive(Clone, Debug)]
pub struct GenerateImageRequestBuilder {
/// Which model to use.
pub model: CommonImageModel,
pub model: RouterImageModel,
/// Which provider to use.
pub provider: Provider,
pub provider: RouterProvider,
/// The prompt for the image generation.
pub prompt: Option<String>,
@@ -63,13 +62,13 @@ pub struct GenerateImageRequestBuilder {
pub image_inputs: Option<ImageListRef>,
/// The resolution to use.
pub resolution: Option<CommonResolution>,
pub resolution: Option<RouterResolution>,
/// The aspect ratio to use.
pub aspect_ratio: Option<CommonAspectRatio>,
pub aspect_ratio: Option<RouterAspectRatio>,
/// The quality level for generation. Not all models use this.
pub quality: Option<CommonQuality>,
pub quality: Option<RouterQuality>,
/// How many images to generate.
pub image_batch_count: Option<u16>,
@@ -98,95 +97,62 @@ pub struct GenerateImageRequestBuilder {
impl GenerateImageRequestBuilder {
pub fn use_new_builder(&self) -> bool {
match (self.provider, self.model) {
(Provider::Fal, CommonImageModel::Flux1Dev) => true,
(Provider::Fal, CommonImageModel::Flux1Schnell) => true,
(Provider::Fal, CommonImageModel::GptImage1) => true,
(Provider::Fal, CommonImageModel::GptImage1p5) => true,
(Provider::Fal, CommonImageModel::GptImage2) => true,
(Provider::Fal, CommonImageModel::NanoBanana2) => true,
(Provider::Fal, CommonImageModel::NanoBananaPro) => true,
_ => false,
}
}
/// Build an executable image generation request for the configured
/// (provider, model) pair. Returns either a directly-sendable `Request`
/// or a `Draft` that needs media-token resolution before sending.
///
/// Named `build2` for historical reasons — this is the only build method
/// since v1 was removed. The name is preserved for consistency with
/// `GenerateVideoRequestBuilder::build2`.
pub fn build2(self) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
match (self.provider, self.model) {
(Provider::Fal, CommonImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
(Provider::Fal, CommonImageModel::Flux1Schnell) => build_fal_flux_1_schnell(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::NanoBanana2) => build_fal_nano_banana_2(self),
(Provider::Fal, CommonImageModel::NanoBananaPro) => build_fal_nano_banana_pro(self),
(RouterProvider::Artcraft, RouterImageModel::Flux1Dev) => build_artcraft_flux_1_dev(self),
(RouterProvider::Artcraft, RouterImageModel::Flux1Schnell) => build_artcraft_flux_1_schnell(self),
(RouterProvider::Artcraft, RouterImageModel::FluxPro11) => build_artcraft_flux_pro_1p1(self),
(RouterProvider::Artcraft, RouterImageModel::FluxPro11Ultra) => build_artcraft_flux_pro_1p1_ultra(self),
(RouterProvider::Artcraft, RouterImageModel::GptImage1) => build_artcraft_gpt_image_1(self),
(RouterProvider::Artcraft, RouterImageModel::GptImage1p5) => build_artcraft_gpt_image_1p5(self),
(RouterProvider::Artcraft, RouterImageModel::GptImage2) => build_artcraft_gpt_image_2(self),
(RouterProvider::Artcraft, RouterImageModel::NanoBanana) => build_artcraft_nano_banana(self),
(RouterProvider::Artcraft, RouterImageModel::NanoBanana2) => build_artcraft_nano_banana_2(self),
(RouterProvider::Artcraft, RouterImageModel::NanoBananaPro) => build_artcraft_nano_banana_pro(self),
(RouterProvider::Artcraft, RouterImageModel::Seedream4) => build_artcraft_seedream_4(self),
(RouterProvider::Artcraft, RouterImageModel::Seedream4p5) => build_artcraft_seedream_4p5(self),
(RouterProvider::Artcraft, RouterImageModel::Seedream5Lite) => build_artcraft_seedream_5_lite(self),
(RouterProvider::Artcraft, RouterImageModel::QwenEdit2511Angles) => build_artcraft_qwen_edit_2511_angles(self),
(RouterProvider::Artcraft, RouterImageModel::Flux2LoraAngles) => build_artcraft_flux_2_lora_angles(self),
(RouterProvider::Artcraft, RouterImageModel::Midjourney7) => build_artcraft_midjourney_7(self),
(RouterProvider::Artcraft, RouterImageModel::Midjourney7Niji) => build_artcraft_midjourney_7_niji(self),
(RouterProvider::Artcraft, RouterImageModel::Midjourney8) => build_artcraft_midjourney_8(self),
(RouterProvider::Fal, RouterImageModel::Flux1Dev) => build_fal_flux_1_dev(self),
(RouterProvider::Fal, RouterImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self),
(RouterProvider::Fal, RouterImageModel::FluxPro11) => build_fal_flux_pro_1p1(self),
(RouterProvider::Fal, RouterImageModel::FluxPro11Ultra) => build_fal_flux_pro_1p1_ultra(self),
(RouterProvider::Fal, RouterImageModel::GptImage1) => build_fal_gpt_image_1(self),
(RouterProvider::Fal, RouterImageModel::GptImage1p5) => build_fal_gpt_image_1p5(self),
(RouterProvider::Fal, RouterImageModel::GptImage2) => build_fal_gpt_image_2(self),
(RouterProvider::Fal, RouterImageModel::NanoBanana) => build_fal_nano_banana(self),
(RouterProvider::Fal, RouterImageModel::NanoBanana2) => build_fal_nano_banana_2(self),
(RouterProvider::Fal, RouterImageModel::NanoBananaPro) => build_fal_nano_banana_pro(self),
(RouterProvider::Fal, RouterImageModel::Seedream4) => build_fal_seedream_4(self),
(RouterProvider::Fal, RouterImageModel::Seedream4p5) => build_fal_seedream_4p5(self),
(RouterProvider::Fal, RouterImageModel::Seedream5Lite) => build_fal_seedream_5_lite(self),
(RouterProvider::Fal, RouterImageModel::QwenEdit2511Angles) => build_fal_qwen_edit_2511_angles(self),
(RouterProvider::Fal, RouterImageModel::Flux2LoraAngles) => build_fal_flux_2_lora_angles(self),
// Kinovi / Seedance2Pro (Midjourney)
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7) => build_kinovi_midjourney_7(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney7Niji) => build_kinovi_midjourney_7_niji(self),
(RouterProvider::Seedance2Pro, RouterImageModel::Midjourney8) => build_kinovi_midjourney_8(self),
_ => self.unsupported_provider_and_model(),
}
}
/// Read the image generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
match self.provider {
Provider::Artcraft => self.build_artcraft(),
Provider::Fal => self.build_fal(),
_ => self.unsupported_provider(),
}
}
fn build_artcraft(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
match self.model {
CommonImageModel::Flux1Dev => plan_generate_image_artcraft_flux_1_dev(self),
CommonImageModel::Flux1Schnell => plan_generate_image_artcraft_flux_1_schnell(self),
CommonImageModel::Flux2LoraAngles => plan_generate_image_artcraft_flux_2_lora_angles(self),
CommonImageModel::FluxPro11 => plan_generate_image_artcraft_flux_pro_1p1(self),
CommonImageModel::FluxPro11Ultra => plan_generate_image_artcraft_flux_pro_1p1_ultra(self),
CommonImageModel::GptImage1 => plan_generate_image_artcraft_gpt_image_1(self),
CommonImageModel::GptImage1p5 => plan_generate_image_artcraft_gpt_image_1p5(self),
CommonImageModel::GptImage2 => plan_generate_image_artcraft_gpt_image_2(self),
CommonImageModel::NanoBanana => plan_generate_image_artcraft_nano_banana(self),
CommonImageModel::NanoBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
CommonImageModel::NanoBananaPro => plan_generate_image_artcraft_nano_banana_pro(self),
CommonImageModel::QwenEdit2511Angles => plan_generate_image_artcraft_qwen_edit_2511_angles(self),
CommonImageModel::Seedream4 => plan_generate_image_artcraft_seedream_4(self),
CommonImageModel::Seedream4p5 => plan_generate_image_artcraft_seedream_4p5(self),
CommonImageModel::Seedream5Lite => plan_generate_image_artcraft_seedream_5_lite(self),
_ => Err(ArtcraftRouterError::UnsupportedModel(format!("{:?}", self.model))),
}
}
fn build_fal(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
match self.model {
CommonImageModel::Flux1Dev => plan_generate_image_fal_flux_1_dev(self),
CommonImageModel::Flux1Schnell => plan_generate_image_fal_flux_1_schnell(self),
CommonImageModel::FluxPro11 => plan_generate_image_fal_flux_pro_1p1(self),
CommonImageModel::FluxPro11Ultra => plan_generate_image_fal_flux_pro_1p1_ultra(self),
CommonImageModel::GptImage1 => plan_generate_image_fal_gpt_image_1(self),
CommonImageModel::GptImage1p5 => plan_generate_image_fal_gpt_image_1p5(self),
CommonImageModel::GptImage2 => plan_generate_image_fal_gpt_image_2(self),
CommonImageModel::NanoBanana => plan_generate_image_fal_nano_banana(self),
CommonImageModel::NanoBanana2 => plan_generate_image_fal_nano_banana_2(self),
CommonImageModel::NanoBananaPro => plan_generate_image_fal_nano_banana_pro(self),
CommonImageModel::Seedream4 => plan_generate_image_fal_seedream_4(self),
CommonImageModel::Seedream4p5 => plan_generate_image_fal_seedream_4p5(self),
CommonImageModel::Seedream5Lite => plan_generate_image_fal_seedream_5_lite(self),
_ => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "provider",
value: format!("{:?} is only available on the Artcraft provider", self.model),
}))
}
}
}
fn unsupported_provider(&self) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
Err(ArtcraftRouterError::UnsupportedModel(
format!("Image generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
))
}
fn unsupported_provider_and_model(&self) -> Result<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
Err(ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(
format!("Image generation for model `{:?}` is not supported for provider {:?} using the new API", self.model, self.provider)
format!("Image generation for model `{:?}` is not supported for provider {:?}", self.model, self.provider)
))
}
@@ -25,10 +25,22 @@ pub struct FalImageResponsePayload {
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
}
/// Response from the Seedance2Pro/Kinovi provider (used for Midjourney
/// image generation). Mirrors `Seedance2proVideoResponsePayload` on the
/// video side.
#[derive(Clone, Debug)]
pub struct Seedance2proImageResponsePayload {
pub order_id: String,
pub task_id: String,
pub maybe_order_ids: Option<Vec<String>>,
pub maybe_task_ids: Option<Vec<String>>,
}
#[derive(Clone, Debug)]
pub enum GenerateImageResponse {
Artcraft(ArtcraftImageResponsePayload),
Fal(FalImageResponsePayload),
Seedance2Pro(Seedance2proImageResponsePayload),
}
impl GenerateImageResponse {
@@ -45,4 +57,11 @@ impl GenerateImageResponse {
_ => None,
}
}
pub fn get_seedance2pro_payload(&self) -> Option<Seedance2proImageResponsePayload> {
match self {
Self::Seedance2Pro(p) => Some(p.clone()),
_ => None,
}
}
}
@@ -0,0 +1,60 @@
use crate::api::router_provider::RouterProvider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::image_generation_draft_context::ImageGenerationDraftContext;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7::draft::KinoviMidjourney7DraftState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::draft::KinoviMidjourney7NijiDraftState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::draft::KinoviMidjourney8DraftState;
/// Wrapper for all image generation draft requests.
///
/// Drafts hold validated/planned parameters but may still need media
/// resolution (e.g. uploading local files to a provider) before they can
/// be sent. Today only Kinovi/Midjourney needs this — when the caller
/// supplies reference images, we must upload them to the Seedance2Pro
/// CDN before sending the request.
#[derive(Clone, Debug)]
pub enum ImageGenerationDraftRequest {
KinoviMidjourney7(KinoviMidjourney7DraftState),
KinoviMidjourney7Niji(KinoviMidjourney7NijiDraftState),
KinoviMidjourney8(KinoviMidjourney8DraftState),
}
impl ImageGenerationDraftRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
}
}
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
match self {
Self::KinoviMidjourney7(draft) => Ok(KinoviMidjourney7CostState::from_draft(draft).estimate_cost()),
Self::KinoviMidjourney7Niji(draft) => Ok(KinoviMidjourney7NijiCostState::from_draft(draft).estimate_cost()),
Self::KinoviMidjourney8(draft) => Ok(KinoviMidjourney8CostState::from_draft(draft).estimate_cost()),
}
}
pub async fn finalize(self, draft_context: ImageGenerationDraftContext<'_>) -> Result<ImageGenerationRequest, ArtcraftRouterError> {
match self {
Self::KinoviMidjourney7(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(ImageGenerationRequest::KinoviMidjourney7(result))
}
Self::KinoviMidjourney7Niji(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(ImageGenerationRequest::KinoviMidjourney7Niji(result))
}
Self::KinoviMidjourney8(mut draft) => {
let result = draft.to_request(&draft_context).await?;
Ok(ImageGenerationRequest::KinoviMidjourney8(result))
}
}
}
}
@@ -0,0 +1,46 @@
use std::collections::HashMap;
use std::fmt::Debug;
use tokens::tokens::media_files::MediaFileToken;
use crate::client::router_client::RouterClient;
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
/// Context passed when finalizing an image draft into a concrete request.
///
/// Drafts only exist for providers that need to upload assets before
/// sending the request. Today that's the Seedance2Pro/Kinovi pipeline
/// (used for Midjourney image generation with reference images); other
/// providers — Artcraft, Fal — accept image URLs directly and skip the
/// draft phase entirely.
#[derive(Clone, Default)]
pub struct ImageGenerationDraftContext<'a> {
/// Optional: the router client, needed for providers that require
/// authenticated calls (e.g. Seedance2Pro/Kinovi uploads).
pub client: Option<&'a RouterClient>,
/// Optional: a map of Media File Tokens to their ArtCraft URLs.
/// Required only when the draft has unresolved `MediaFileToken` image
/// inputs that must be downloaded from ArtCraft and re-uploaded to the
/// target provider's CDN.
pub media_file_to_artcraft_url_map: Option<&'a HashMap<MediaFileToken, String>>,
}
impl<'a> ImageGenerationDraftContext<'a> {
pub fn get_seedance2pro_client_ref(&self) -> Result<&RouterSeedance2ProClient, ArtcraftRouterError> {
let client = self.client.ok_or(ArtcraftRouterError::Client(ClientError::RouterClientNotProvided))?;
client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)
}
}
impl Debug for ImageGenerationDraftContext<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImageGenerationDraftContext")
.field("client", &self.client.is_some())
.field("media_file_to_artcraft_url_map", &self.media_file_to_artcraft_url_map.map(|m| m.len()))
.finish()
}
}
@@ -1,8 +1,8 @@
use crate::api::provider::Provider;
use crate::api::router_provider::RouterProvider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image_v2::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image_v2::image_generation_request::ImageGenerationRequest;
use crate::generate::generate_image::image_generation_draft::ImageGenerationDraftRequest;
use crate::generate::generate_image::image_generation_request::ImageGenerationRequest;
#[derive(Debug, Clone)]
pub enum ImageGenerationDraftOrRequest {
@@ -11,7 +11,7 @@ pub enum ImageGenerationDraftOrRequest {
}
impl ImageGenerationDraftOrRequest {
pub fn get_provider(&self) -> Provider {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::Draft(draft) => draft.get_provider(),
Self::Request(request) => request.get_provider(),
@@ -1,331 +0,0 @@
use crate::client::router_client::RouterClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_1_dev::estimate_image_cost_artcraft_flux_1_dev;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_1_schnell::estimate_image_cost_artcraft_flux_1_schnell;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1::estimate_image_cost_artcraft_flux_pro_1p1;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1_ultra::estimate_image_cost_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1::estimate_image_cost_artcraft_gpt_image_1;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_1p5::estimate_image_cost_artcraft_gpt_image_1p5;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_2::estimate_image_cost_artcraft_gpt_image_2;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana::estimate_image_cost_artcraft_nano_banana;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_2::estimate_image_cost_artcraft_nano_banana_2;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_pro::estimate_image_cost_artcraft_nano_banana_pro;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_qwen_edit_2511_angles::estimate_image_cost_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_2_lora_angles::estimate_image_cost_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_dev::estimate_image_cost_fal_flux_1_dev;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_1_schnell::estimate_image_cost_fal_flux_1_schnell;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro_1p1::estimate_image_cost_fal_flux_pro_1p1;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_flux_pro_1p1_ultra::estimate_image_cost_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1::estimate_image_cost_fal_gpt_image_1;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_1p5::estimate_image_cost_fal_gpt_image_1p5;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_gpt_image_2::estimate_image_cost_fal_gpt_image_2;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4::estimate_image_cost_fal_seedream_4;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_4p5::estimate_image_cost_fal_seedream_4p5;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_seedream_5_lite::estimate_image_cost_fal_seedream_5_lite;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana::estimate_image_cost_fal_nano_banana;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_2::estimate_image_cost_fal_nano_banana_2;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1::execute_artcraft_flux_pro_1p1;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1_ultra::execute_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1::execute_artcraft_gpt_image_1;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_1p5::execute_artcraft_gpt_image_1p5;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_2::execute_artcraft_gpt_image_2;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana::execute_artcraft_nano_banana;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_2::execute_artcraft_nano_banana_2;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_pro::execute_artcraft_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_qwen_edit_2511_angles::execute_artcraft_qwen_edit_2511_angles;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_2_lora_angles::execute_artcraft_flux_2_lora_angles;
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_dev::execute_fal_flux_1_dev;
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_1_schnell::execute_fal_flux_1_schnell;
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1p1::execute_fal_flux_pro_1p1;
use crate::generate::generate_image::execute::fal::generate_image_fal_flux_pro_1p1_ultra::execute_fal_flux_pro_1p1_ultra;
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1::execute_fal_gpt_image_1;
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_1p5::execute_fal_gpt_image_1p5;
use crate::generate::generate_image::execute::fal::generate_image_fal_gpt_image_2::execute_fal_gpt_image_2;
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4::execute_fal_seedream_4;
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_4p5::execute_fal_seedream_4p5;
use crate::generate::generate_image::execute::fal::generate_image_fal_seedream_5_lite::execute_fal_seedream_5_lite;
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana::execute_fal_nano_banana;
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_2::execute_fal_nano_banana_2;
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1::PlanArtcraftGptImage1;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_2::PlanArtcraftGptImage2;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_qwen_edit_2511_angles::PlanArtcraftQwenEdit2511Angles;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_2_lora_angles::PlanArtcraftFlux2LoraAngles;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_dev::PlanFalFlux1Dev;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_1_schnell::PlanFalFlux1Schnell;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1::PlanFalFluxPro11;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_flux_pro_1p1_ultra::PlanFalFluxPro11Ultra;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1::PlanFalGptImage1;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_1p5::PlanFalGptImage1p5;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_gpt_image_2::PlanFalGptImage2;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4::PlanFalSeedream4;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_4p5::PlanFalSeedream4p5;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_seedream_5_lite::PlanFalSeedream5Lite;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana::PlanFalNanoBanana;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_2::PlanFalNanaBanana2;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
#[derive(Debug)]
pub enum ImageGenerationPlan {
ArtcraftFlux1Dev(PlanArtcraftFlux1Dev),
ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell),
ArtcraftFluxPro11(PlanArtcraftFluxPro11),
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra),
ArtcraftGptImage1(PlanArtcraftGptImage1),
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5),
ArtcraftGptImage2(PlanArtcraftGptImage2),
ArtcraftNanaBanana(PlanArtcraftNanaBanana),
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2),
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro),
ArtcraftSeedream4(PlanArtcraftSeedream4),
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5),
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite),
ArtcraftQwenEdit2511Angles(PlanArtcraftQwenEdit2511Angles),
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles),
FalFlux1Dev(PlanFalFlux1Dev),
FalFlux1Schnell(PlanFalFlux1Schnell),
FalFluxPro11(PlanFalFluxPro11),
FalFluxPro11Ultra(PlanFalFluxPro11Ultra),
FalGptImage1(PlanFalGptImage1),
FalGptImage1p5(PlanFalGptImage1p5),
FalGptImage2(PlanFalGptImage2),
FalSeedream4(PlanFalSeedream4),
FalSeedream4p5(PlanFalSeedream4p5),
FalSeedream5Lite(PlanFalSeedream5Lite),
FalNanoBanana(PlanFalNanoBanana),
FalNanaBanana2(PlanFalNanaBanana2),
FalNanaBananaPro(PlanFalNanaBananaPro),
}
impl ImageGenerationPlan {
pub async fn generate_image(
&self,
client: &RouterClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
ImageGenerationPlan::ArtcraftFlux1Dev(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_1_dev(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFlux1Schnell(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_1_schnell(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFluxPro11(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_pro_1p1(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_pro_1p1_ultra(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftGptImage1(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_gpt_image_1(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_gpt_image_1p5(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftGptImage2(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_gpt_image_2(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftNanaBanana(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftNanaBanana2(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana_2(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana_pro(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream4(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_4(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream4p5(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_4p5(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_5_lite(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_qwen_edit_2511_angles(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_2_lora_angles(plan, artcraft_client).await
}
ImageGenerationPlan::FalFlux1Dev(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_flux_1_dev(plan, fal_client).await
}
ImageGenerationPlan::FalFlux1Schnell(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_flux_1_schnell(plan, fal_client).await
}
ImageGenerationPlan::FalFluxPro11(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_flux_pro_1p1(plan, fal_client).await
}
ImageGenerationPlan::FalFluxPro11Ultra(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_flux_pro_1p1_ultra(plan, fal_client).await
}
ImageGenerationPlan::FalGptImage1(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_gpt_image_1(plan, fal_client).await
}
ImageGenerationPlan::FalGptImage1p5(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_gpt_image_1p5(plan, fal_client).await
}
ImageGenerationPlan::FalGptImage2(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_gpt_image_2(plan, fal_client).await
}
ImageGenerationPlan::FalSeedream4(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_seedream_4(plan, fal_client).await
}
ImageGenerationPlan::FalSeedream4p5(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_seedream_4p5(plan, fal_client).await
}
ImageGenerationPlan::FalSeedream5Lite(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_seedream_5_lite(plan, fal_client).await
}
ImageGenerationPlan::FalNanoBanana(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_nano_banana(plan, fal_client).await
}
ImageGenerationPlan::FalNanaBanana2(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_nano_banana_2(plan, fal_client).await
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_nano_banana_pro(plan, fal_client).await
}
}
}
pub fn estimate_costs(&self) -> ImageGenerationCostEstimate {
match self {
ImageGenerationPlan::ArtcraftFlux1Dev(plan) => {
estimate_image_cost_artcraft_flux_1_dev(plan)
}
ImageGenerationPlan::ArtcraftFlux1Schnell(plan) => {
estimate_image_cost_artcraft_flux_1_schnell(plan)
}
ImageGenerationPlan::ArtcraftFluxPro11(plan) => {
estimate_image_cost_artcraft_flux_pro_1p1(plan)
}
ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) => {
estimate_image_cost_artcraft_flux_pro_1p1_ultra(plan)
}
ImageGenerationPlan::ArtcraftGptImage1(plan) => {
estimate_image_cost_artcraft_gpt_image_1(plan)
}
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
estimate_image_cost_artcraft_gpt_image_1p5(plan)
}
ImageGenerationPlan::ArtcraftGptImage2(plan) => {
estimate_image_cost_artcraft_gpt_image_2(plan)
}
ImageGenerationPlan::ArtcraftNanaBanana(plan) => {
estimate_image_cost_artcraft_nano_banana(plan)
}
ImageGenerationPlan::ArtcraftNanaBanana2(plan) => {
estimate_image_cost_artcraft_nano_banana_2(plan)
}
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
estimate_image_cost_artcraft_nano_banana_pro(plan)
}
ImageGenerationPlan::ArtcraftSeedream4(plan) => {
estimate_image_cost_artcraft_seedream_4(plan)
}
ImageGenerationPlan::ArtcraftSeedream4p5(plan) => {
estimate_image_cost_artcraft_seedream_4p5(plan)
}
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
estimate_image_cost_artcraft_seedream_5_lite(plan)
}
ImageGenerationPlan::ArtcraftQwenEdit2511Angles(plan) => {
estimate_image_cost_artcraft_qwen_edit_2511_angles(plan)
}
ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) => {
estimate_image_cost_artcraft_flux_2_lora_angles(plan)
}
ImageGenerationPlan::FalFlux1Dev(plan) => {
estimate_image_cost_fal_flux_1_dev(plan)
}
ImageGenerationPlan::FalFlux1Schnell(plan) => {
estimate_image_cost_fal_flux_1_schnell(plan)
}
ImageGenerationPlan::FalFluxPro11(plan) => {
estimate_image_cost_fal_flux_pro_1p1(plan)
}
ImageGenerationPlan::FalFluxPro11Ultra(plan) => {
estimate_image_cost_fal_flux_pro_1p1_ultra(plan)
}
ImageGenerationPlan::FalGptImage1(plan) => {
estimate_image_cost_fal_gpt_image_1(plan)
}
ImageGenerationPlan::FalGptImage1p5(plan) => {
estimate_image_cost_fal_gpt_image_1p5(plan)
}
ImageGenerationPlan::FalGptImage2(plan) => {
estimate_image_cost_fal_gpt_image_2(plan)
}
ImageGenerationPlan::FalSeedream4(plan) => {
estimate_image_cost_fal_seedream_4(plan)
}
ImageGenerationPlan::FalSeedream4p5(plan) => {
estimate_image_cost_fal_seedream_4p5(plan)
}
ImageGenerationPlan::FalSeedream5Lite(plan) => {
estimate_image_cost_fal_seedream_5_lite(plan)
}
ImageGenerationPlan::FalNanoBanana(plan) => {
estimate_image_cost_fal_nano_banana(plan)
}
ImageGenerationPlan::FalNanaBanana2(plan) => {
estimate_image_cost_fal_nano_banana_2(plan)
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
estimate_image_cost_fal_nano_banana_pro(plan)
}
}
}
}
@@ -0,0 +1,410 @@
use crate::api::router_provider::RouterProvider;
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::providers::artcraft::flux_1_dev::cost::ArtcraftFlux1DevCostState;
use crate::generate::generate_image::providers::artcraft::flux_1_dev::request::ArtcraftFlux1DevRequestState;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::cost::ArtcraftFlux1SchnellCostState;
use crate::generate::generate_image::providers::artcraft::flux_1_schnell::request::ArtcraftFlux1SchnellRequestState;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::cost::ArtcraftFlux2LoraAnglesCostState;
use crate::generate::generate_image::providers::artcraft::flux_2_lora_angles::request::ArtcraftFlux2LoraAnglesRequestState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::cost::ArtcraftFluxPro1p1CostState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1::request::ArtcraftFluxPro1p1RequestState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::cost::ArtcraftFluxPro1p1UltraCostState;
use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::request::ArtcraftFluxPro1p1UltraRequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::cost::ArtcraftGptImage1CostState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1::request::ArtcraftGptImage1RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::cost::ArtcraftGptImage1p5CostState;
use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::cost::ArtcraftGptImage2CostState;
use crate::generate::generate_image::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState;
use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState;
use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::cost::ArtcraftMidjourney7NijiCostState;
use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState;
use crate::generate::generate_image::providers::artcraft::midjourney_8::cost::ArtcraftMidjourney8CostState;
use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana::cost::ArtcraftNanoBananaCostState;
use crate::generate::generate_image::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana_2::cost::ArtcraftNanoBanana2CostState;
use crate::generate::generate_image::providers::artcraft::nano_banana_2::request::ArtcraftNanoBanana2RequestState;
use crate::generate::generate_image::providers::artcraft::nano_banana_pro::cost::ArtcraftNanoBananaProCostState;
use crate::generate::generate_image::providers::artcraft::nano_banana_pro::request::ArtcraftNanoBananaProRequestState;
use crate::generate::generate_image::providers::artcraft::qwen_edit_2511_angles::cost::ArtcraftQwenEdit2511AnglesCostState;
use crate::generate::generate_image::providers::artcraft::qwen_edit_2511_angles::request::ArtcraftQwenEdit2511AnglesRequestState;
use crate::generate::generate_image::providers::artcraft::seedream_4::cost::ArtcraftSeedream4CostState;
use crate::generate::generate_image::providers::artcraft::seedream_4::request::ArtcraftSeedream4RequestState;
use crate::generate::generate_image::providers::artcraft::seedream_4p5::cost::ArtcraftSeedream4p5CostState;
use crate::generate::generate_image::providers::artcraft::seedream_4p5::request::ArtcraftSeedream4p5RequestState;
use crate::generate::generate_image::providers::artcraft::seedream_5_lite::cost::ArtcraftSeedream5LiteCostState;
use crate::generate::generate_image::providers::artcraft::seedream_5_lite::request::ArtcraftSeedream5LiteRequestState;
use crate::generate::generate_image::providers::fal::flux_1_dev::cost::FalFlux1DevCostState;
use crate::generate::generate_image::providers::fal::flux_1_dev::request::FalFlux1DevRequestState;
use crate::generate::generate_image::providers::fal::flux_1_schnell::cost::FalFlux1SchnellCostState;
use crate::generate::generate_image::providers::fal::flux_1_schnell::request::FalFlux1SchnellRequestState;
use crate::generate::generate_image::providers::fal::flux_2_lora_angles::cost::FalFlux2LoraAnglesCostState;
use crate::generate::generate_image::providers::fal::flux_2_lora_angles::request::FalFlux2LoraAnglesRequestState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1::cost::FalFluxPro1p1CostState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1::request::FalFluxPro1p1RequestState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1_ultra::cost::FalFluxPro1p1UltraCostState;
use crate::generate::generate_image::providers::fal::flux_pro_1p1_ultra::request::FalFluxPro1p1UltraRequestState;
use crate::generate::generate_image::providers::fal::gpt_image_1::cost::FalGptImage1CostState;
use crate::generate::generate_image::providers::fal::gpt_image_1::request::FalGptImage1RequestState;
use crate::generate::generate_image::providers::fal::gpt_image_1p5::cost::FalGptImage1p5CostState;
use crate::generate::generate_image::providers::fal::gpt_image_1p5::request::FalGptImage1p5RequestState;
use crate::generate::generate_image::providers::fal::gpt_image_2::cost::FalGptImage2CostState;
use crate::generate::generate_image::providers::fal::gpt_image_2::request::FalGptImage2RequestState;
use crate::generate::generate_image::providers::fal::nano_banana::cost::FalNanoBananaCostState;
use crate::generate::generate_image::providers::fal::nano_banana::request::FalNanoBananaRequestState;
use crate::generate::generate_image::providers::fal::nano_banana_2::cost::FalNanoBanana2CostState;
use crate::generate::generate_image::providers::fal::nano_banana_2::request::FalNanoBanana2RequestState;
use crate::generate::generate_image::providers::fal::nano_banana_pro::cost::FalNanoBananaProCostState;
use crate::generate::generate_image::providers::fal::nano_banana_pro::request::FalNanoBananaProRequestState;
use crate::generate::generate_image::providers::fal::qwen_edit_2511_angles::cost::FalQwenEdit2511AnglesCostState;
use crate::generate::generate_image::providers::fal::qwen_edit_2511_angles::request::FalQwenEdit2511AnglesRequestState;
use crate::generate::generate_image::providers::fal::seedream_4::cost::FalSeedream4CostState;
use crate::generate::generate_image::providers::fal::seedream_4::request::FalSeedream4RequestState;
use crate::generate::generate_image::providers::fal::seedream_4p5::cost::FalSeedream4p5CostState;
use crate::generate::generate_image::providers::fal::seedream_4p5::request::FalSeedream4p5RequestState;
use crate::generate::generate_image::providers::fal::seedream_5_lite::cost::FalSeedream5LiteCostState;
use crate::generate::generate_image::providers::fal::seedream_5_lite::request::FalSeedream5LiteRequestState;
use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7::request::KinoviMidjourney7RequestState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState;
use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::request::KinoviMidjourney7NijiRequestState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState;
use crate::generate::generate_image::providers::kinovi::midjourney_8::request::KinoviMidjourney8RequestState;
#[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),
ArtcraftMidjourney7(ArtcraftMidjourney7RequestState),
ArtcraftMidjourney7Niji(ArtcraftMidjourney7NijiRequestState),
ArtcraftMidjourney8(ArtcraftMidjourney8RequestState),
// ── 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),
// ── Kinovi / Seedance2Pro provider (Midjourney image generation) ──
KinoviMidjourney7(KinoviMidjourney7RequestState),
KinoviMidjourney7Niji(KinoviMidjourney7NijiRequestState),
KinoviMidjourney8(KinoviMidjourney8RequestState),
}
impl ImageGenerationRequest {
pub fn get_provider(&self) -> RouterProvider {
match self {
Self::ArtcraftFlux1Dev(_) => RouterProvider::Artcraft,
Self::ArtcraftFlux1Schnell(_) => RouterProvider::Artcraft,
Self::ArtcraftFluxPro1p1(_) => RouterProvider::Artcraft,
Self::ArtcraftFluxPro1p1Ultra(_) => RouterProvider::Artcraft,
Self::ArtcraftGptImage1(_) => RouterProvider::Artcraft,
Self::ArtcraftGptImage1p5(_) => RouterProvider::Artcraft,
Self::ArtcraftGptImage2(_) => RouterProvider::Artcraft,
Self::ArtcraftNanoBanana(_) => RouterProvider::Artcraft,
Self::ArtcraftNanoBanana2(_) => RouterProvider::Artcraft,
Self::ArtcraftNanoBananaPro(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedream4(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedream4p5(_) => RouterProvider::Artcraft,
Self::ArtcraftSeedream5Lite(_) => RouterProvider::Artcraft,
Self::ArtcraftQwenEdit2511Angles(_) => RouterProvider::Artcraft,
Self::ArtcraftFlux2LoraAngles(_) => RouterProvider::Artcraft,
Self::ArtcraftMidjourney7(_) => RouterProvider::Artcraft,
Self::ArtcraftMidjourney7Niji(_) => RouterProvider::Artcraft,
Self::ArtcraftMidjourney8(_) => RouterProvider::Artcraft,
Self::FalFlux1Dev(_) => RouterProvider::Fal,
Self::FalFlux1Schnell(_) => RouterProvider::Fal,
Self::FalFluxPro1p1(_) => RouterProvider::Fal,
Self::FalFluxPro1p1Ultra(_) => RouterProvider::Fal,
Self::FalGptImage1(_) => RouterProvider::Fal,
Self::FalGptImage1p5(_) => RouterProvider::Fal,
Self::FalGptImage2(_) => RouterProvider::Fal,
Self::FalNanoBanana(_) => RouterProvider::Fal,
Self::FalNanoBanana2(_) => RouterProvider::Fal,
Self::FalNanoBananaPro(_) => RouterProvider::Fal,
Self::FalSeedream4(_) => RouterProvider::Fal,
Self::FalSeedream4p5(_) => RouterProvider::Fal,
Self::FalSeedream5Lite(_) => RouterProvider::Fal,
Self::FalQwenEdit2511Angles(_) => RouterProvider::Fal,
Self::FalFlux2LoraAngles(_) => RouterProvider::Fal,
Self::KinoviMidjourney7(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney7Niji(_) => RouterProvider::Seedance2Pro,
Self::KinoviMidjourney8(_) => RouterProvider::Seedance2Pro,
}
}
pub fn estimate_cost(&self) -> Result<ImageGenerationCostEstimate, ArtcraftRouterError> {
match self {
// ── Artcraft ──
Self::ArtcraftFlux1Dev(request) => {
Ok(ArtcraftFlux1DevCostState::from_request(request).estimate_cost())
}
Self::ArtcraftFlux1Schnell(request) => {
Ok(ArtcraftFlux1SchnellCostState::from_request(request).estimate_cost())
}
Self::ArtcraftFluxPro1p1(request) => {
Ok(ArtcraftFluxPro1p1CostState::from_request(request).estimate_cost())
}
Self::ArtcraftFluxPro1p1Ultra(request) => {
Ok(ArtcraftFluxPro1p1UltraCostState::from_request(request).estimate_cost())
}
Self::ArtcraftGptImage1(request) => {
Ok(ArtcraftGptImage1CostState::from_request(request).estimate_cost())
}
Self::ArtcraftGptImage1p5(request) => {
Ok(ArtcraftGptImage1p5CostState::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())
}
Self::ArtcraftMidjourney7(request) => {
Ok(ArtcraftMidjourney7CostState::from_request(request).estimate_cost())
}
Self::ArtcraftMidjourney7Niji(request) => {
Ok(ArtcraftMidjourney7NijiCostState::from_request(request).estimate_cost())
}
Self::ArtcraftMidjourney8(request) => {
Ok(ArtcraftMidjourney8CostState::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()),
Self::KinoviMidjourney7(request) => Ok(KinoviMidjourney7CostState::from_request(request).estimate_cost()),
Self::KinoviMidjourney7Niji(request) => Ok(KinoviMidjourney7NijiCostState::from_request(request).estimate_cost()),
Self::KinoviMidjourney8(request) => Ok(KinoviMidjourney8CostState::from_request(request).estimate_cost()),
}
}
pub async fn send_request(&self, client: &RouterClient) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
// ── Artcraft (omni-gen image endpoint) ──
Self::ArtcraftFlux1Dev(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftFlux1Schnell(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftFluxPro1p1(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftFluxPro1p1Ultra(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftGptImage1(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftGptImage1p5(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftGptImage2(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftNanoBanana(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftNanoBanana2(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftNanoBananaPro(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftSeedream4(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftSeedream4p5(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftSeedream5Lite(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftQwenEdit2511Angles(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftFlux2LoraAngles(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftMidjourney7(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftMidjourney7Niji(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
Self::ArtcraftMidjourney8(request) => {
let artcraft_client = client.get_artcraft_client_ref()?;
request.send(artcraft_client).await
}
// ── Fal ──
Self::FalFlux1Dev(request) => {
let fal_client = client.get_fal_client_ref()?;
request.send(fal_client).await
}
Self::FalFlux1Schnell(request) => {
let fal_client = client.get_fal_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_client_ref()?;
request.send(fal_client).await
}
Self::FalGptImage1p5(request) => {
let fal_client = client.get_fal_client_ref()?;
request.send(fal_client).await
}
Self::FalGptImage2(request) => {
let fal_client = client.get_fal_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_client_ref()?;
request.send(fal_client).await
}
Self::FalNanoBananaPro(request) => {
let fal_client = client.get_fal_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
}
// ── Kinovi / Seedance2Pro (Midjourney) ──
Self::KinoviMidjourney7(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney7Niji(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
Self::KinoviMidjourney8(request) => {
let seedance_client = client.get_seedance2pro_client_ref()
.map_err(ArtcraftRouterError::Client)?;
request.send(seedance_client).await
}
}
}
}
@@ -1,7 +1,8 @@
pub mod cost;
pub mod execute;
pub mod generate_image_request_builder;
pub mod generate_image_response;
pub mod image_generation_cost_estimate;
pub mod image_generation_plan;
pub mod plan;
pub mod image_generation_draft;
pub mod image_generation_draft_context;
pub mod image_generation_draft_or_request;
pub mod image_generation_request;
pub mod providers;
@@ -1,15 +0,0 @@
pub mod plan_generate_image_artcraft_flux_1_dev;
pub mod plan_generate_image_artcraft_flux_1_schnell;
pub mod plan_generate_image_artcraft_flux_pro_1p1;
pub mod plan_generate_image_artcraft_flux_pro_1p1_ultra;
pub mod plan_generate_image_artcraft_gpt_image_1;
pub mod plan_generate_image_artcraft_gpt_image_1p5;
pub mod plan_generate_image_artcraft_gpt_image_2;
pub mod plan_generate_image_artcraft_nano_banana;
pub mod plan_generate_image_artcraft_nano_banana_2;
pub mod plan_generate_image_artcraft_nano_banana_pro;
pub mod plan_generate_image_artcraft_seedream_4;
pub mod plan_generate_image_artcraft_seedream_4p5;
pub mod plan_generate_image_artcraft_seedream_5_lite;
pub mod plan_generate_image_artcraft_qwen_edit_2511_angles;
pub mod plan_generate_image_artcraft_flux_2_lora_angles;
@@ -1,326 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
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::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages,
};
#[derive(Debug, Clone)]
pub struct PlanArtcraftFlux1Dev {
pub prompt: Option<String>,
pub aspect_ratio: Option<GenerateFlux1DevTextToImageAspectRatio>,
pub num_images: GenerateFlux1DevTextToImageNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_1_dev(
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
// Flux 1 Dev is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
if let Some(GenerationModeMismatchStrategy::AbortGeneration) = request.generation_mode_mismatch_strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux 1 Dev is a text-to-image model and does not support image editing".to_string(),
}));
}
// None or GenerateAnyway: silently ignore image inputs
}
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::ArtcraftFlux1Dev(PlanArtcraftFlux1Dev {
prompt: request.prompt.clone(),
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
}))
}
// Flux 1 Dev supported aspect ratios:
// Square, SquareHd, LandscapeFourByThree, LandscapeSixteenByNine,
// PortraitThreeByFour, PortraitNineBySixteen
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GenerateFlux1DevTextToImageAspectRatio>, ArtcraftRouterError> {
use GenerateFlux1DevTextToImageAspectRatio as FlAr;
match aspect_ratio {
None => Ok(None),
// Auto: not valid for text-to-image; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Some(FlAr::Square)),
// Direct mappings
Some(CommonAspectRatio::Square) => Ok(Some(FlAr::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(FlAr::SquareHd)),
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideThreeByTwo) => {
Ok(Some(FlAr::LandscapeFourByThree))
}
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideTwentyOneByNine) => {
Ok(Some(FlAr::LandscapeSixteenByNine))
}
Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(FlAr::PortraitThreeByFour)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineByTwentyOne) => {
Ok(Some(FlAr::PortraitNineBySixteen))
}
// Mismatch — TallFourByFive maps to nearest (LandscapeFourByThree per existing desktop behavior)
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> GenerateFlux1DevTextToImageAspectRatio {
use GenerateFlux1DevTextToImageAspectRatio as FlAr;
match aspect_ratio {
// TallFourByFive (0.8) — nearest is LandscapeFourByThree per existing desktop behavior
CommonAspectRatio::TallFourByFive => FlAr::LandscapeFourByThree,
_ => FlAr::Square,
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GenerateFlux1DevTextToImageNumImages, ArtcraftRouterError> {
use GenerateFlux1DevTextToImageNumImages as FlN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FlN::One),
2 => Ok(FlN::Two),
3 => Ok(FlN::Three),
4 => Ok(FlN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FlN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
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::test_helpers::base_flux_1_dev_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio as FlAr,
GenerateFlux1DevTextToImageNumImages as FlN,
};
// ── Generation mode mismatch ─────────────────────────────────────────────
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_1_dev_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_1_dev_image_request()
};
assert!(request.build().is_ok());
}
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: None,
..base_flux_1_dev_image_request()
};
assert!(request.build().is_ok());
}
// ── Aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_auto_falls_back_to_square() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, FlAr::Square),
(CommonAspectRatio::SquareHd, FlAr::SquareHd),
(CommonAspectRatio::WideFourByThree, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideThreeByTwo, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideSixteenByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::Wide, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::TallThreeByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallTwoByThree, FlAr::PortraitThreeByFour),
(CommonAspectRatio::WideFiveByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallNineBySixteen, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::Tall, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_dev_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
matches!(plan.aspect_ratio, Some(FlAr::LandscapeFourByThree)),
"expected LandscapeFourByThree nearest match with {:?}", strategy,
);
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_dev_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(matches!(plan.num_images, FlN::Four));
}
}
}
@@ -1,322 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
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::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages,
};
#[derive(Debug, Clone)]
pub struct PlanArtcraftFlux1Schnell {
pub prompt: Option<String>,
pub aspect_ratio: Option<GenerateFlux1SchnellTextToImageAspectRatio>,
pub num_images: GenerateFlux1SchnellTextToImageNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_1_schnell(
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
// Flux 1 Schnell is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
if let Some(GenerationModeMismatchStrategy::AbortGeneration) = request.generation_mode_mismatch_strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux 1 Schnell is a text-to-image model and does not support image editing".to_string(),
}));
}
// None or GenerateAnyway: silently ignore image inputs
}
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell {
prompt: request.prompt.clone(),
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
}))
}
// Flux 1 Schnell supported aspect ratios (identical to Flux 1 Dev):
// Square, SquareHd, LandscapeFourByThree, LandscapeSixteenByNine,
// PortraitThreeByFour, PortraitNineBySixteen
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GenerateFlux1SchnellTextToImageAspectRatio>, ArtcraftRouterError> {
use GenerateFlux1SchnellTextToImageAspectRatio as FlAr;
match aspect_ratio {
None => Ok(None),
// Auto: not valid for text-to-image; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Some(FlAr::Square)),
// Direct mappings
Some(CommonAspectRatio::Square) => Ok(Some(FlAr::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(FlAr::SquareHd)),
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideThreeByTwo) => {
Ok(Some(FlAr::LandscapeFourByThree))
}
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideTwentyOneByNine) => {
Ok(Some(FlAr::LandscapeSixteenByNine))
}
Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(FlAr::PortraitThreeByFour)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineByTwentyOne) => {
Ok(Some(FlAr::PortraitNineBySixteen))
}
// Mismatch — TallFourByFive maps to nearest (LandscapeFourByThree per existing desktop behavior)
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> GenerateFlux1SchnellTextToImageAspectRatio {
use GenerateFlux1SchnellTextToImageAspectRatio as FlAr;
match aspect_ratio {
// TallFourByFive (0.8) — nearest is LandscapeFourByThree per existing desktop behavior
CommonAspectRatio::TallFourByFive => FlAr::LandscapeFourByThree,
_ => FlAr::Square,
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GenerateFlux1SchnellTextToImageNumImages, ArtcraftRouterError> {
use GenerateFlux1SchnellTextToImageNumImages as FlN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FlN::One),
2 => Ok(FlN::Two),
3 => Ok(FlN::Three),
4 => Ok(FlN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FlN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
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::test_helpers::base_flux_1_schnell_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio as FlAr,
GenerateFlux1SchnellTextToImageNumImages as FlN,
};
// ── Generation mode mismatch ─────────────────────────────────────────────
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_1_schnell_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_1_schnell_image_request()
};
assert!(request.build().is_ok());
}
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
generation_mode_mismatch_strategy: None,
..base_flux_1_schnell_image_request()
};
assert!(request.build().is_ok());
}
// ── Aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequestBuilder { aspect_ratio: None, ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_auto_falls_back_to_square() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequestBuilder { aspect_ratio: Some(ar), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, FlAr::Square),
(CommonAspectRatio::SquareHd, FlAr::SquareHd),
(CommonAspectRatio::WideFourByThree, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideThreeByTwo, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideSixteenByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::Wide, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::TallThreeByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallTwoByThree, FlAr::PortraitThreeByFour),
(CommonAspectRatio::WideFiveByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallNineBySixteen, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::Tall, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequestBuilder { aspect_ratio: Some(common), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_schnell_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::LandscapeFourByThree)));
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequestBuilder { image_batch_count: Some(count), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_schnell_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.num_images, FlN::Four));
}
}
}
@@ -1,244 +0,0 @@
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::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize,
Flux2LoraEditImageAngleNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftFlux2LoraAngles {
pub image_input: MediaFileToken,
pub horizontal_angle: Option<f64>,
pub vertical_angle: Option<f64>,
pub zoom: Option<f64>,
pub image_size: Option<Flux2LoraEditImageAngleImageSize>,
pub num_images: Flux2LoraEditImageAngleNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_2_lora_angles(
request: &GenerateImageRequestBuilder,
) -> Result<ImageGenerationPlan, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
// Angle models require exactly one input image.
let image_input = resolve_single_image_input(request.image_inputs.clone())?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles {
image_input,
horizontal_angle: request.horizontal_angle,
vertical_angle: request.vertical_angle,
zoom: request.zoom,
image_size,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
}))
}
fn resolve_single_image_input(
image_list_ref: Option<ImageListRef>,
) -> Result<MediaFileToken, ArtcraftRouterError> {
match image_list_ref {
None => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Angle models require exactly one input image".to_string(),
}))
}
Some(ImageListRef::MediaFileTokens(tokens)) => {
if let Some(token) = tokens.first() {
return Ok(token.clone());
}
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))
}
}
}
// Flux 2 LoRA supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<Flux2LoraEditImageAngleImageSize>, ArtcraftRouterError> {
use Flux2LoraEditImageAngleImageSize 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 @ CommonAspectRatio::WideFiveByFour)
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine)
| 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),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::SquareHd)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Flux2LoraEditImageAngleNumImages, ArtcraftRouterError> {
use Flux2LoraEditImageAngleNumImages 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),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::image_list_ref::ImageListRef;
use crate::test_helpers::base_flux_2_lora_angles_image_request;
use artcraft_api_defs::generate::image::angle::flux_2_lora_edit_image_angle::{
Flux2LoraEditImageAngleImageSize as S,
Flux2LoraEditImageAngleNumImages as N,
};
use tokens::tokens::media_files::MediaFileToken;
fn make_token() -> MediaFileToken {
MediaFileToken::new_from_str("test_token_123")
}
#[test]
fn requires_image_input() {
let request = GenerateImageRequestBuilder {
image_inputs: None,
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn rejects_urls() {
let urls = vec!["https://example.com/img.png".to_string()];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::Urls(urls.clone())),
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_err());
}
#[test]
fn accepts_single_media_token() {
let tokens = vec![make_token()];
let request = GenerateImageRequestBuilder {
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
let result = request.build();
assert!(result.is_ok());
}
#[test]
fn image_size_none_is_none() {
let tokens = vec![make_token()];
let request = GenerateImageRequestBuilder {
aspect_ratio: None,
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square_maps_directly() {
let tokens = vec![make_token()];
let request = GenerateImageRequestBuilder {
aspect_ratio: Some(CommonAspectRatio::Square),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn num_images_direct_mapping() {
let tokens = vec![make_token()];
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequestBuilder {
quality: None,
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn angle_fields_are_passed_through() {
let tokens = vec![make_token()];
let request = GenerateImageRequestBuilder {
horizontal_angle: Some(45.0),
vertical_angle: Some(-30.0),
zoom: Some(1.5),
image_inputs: Some(ImageListRef::MediaFileTokens(tokens.clone())),
..base_flux_2_lora_angles_image_request()
};
let ImageGenerationPlan::ArtcraftFlux2LoraAngles(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux2LoraAngles") };
assert_eq!(plan.horizontal_angle, Some(45.0));
assert_eq!(plan.vertical_angle, Some(-30.0));
assert_eq!(plan.zoom, Some(1.5));
}
}

Some files were not shown because too many files have changed in this diff Show More