Compare commits

..
Author SHA1 Message Date
Tanish Manku eb584ff926 surface gallery load errors with message and copy-to-clipboard
When listUserMediaFiles fails (HTTP error, network error, null reference,
or any other exception), the gallery now shows the error message with
a "Copy error" button instead of silently falling through to "No items yet".

This is a diagnostic aid for a macOS-only blank gallery bug where the
root cause is unknown (CORS, session cookie, or runtime exception) and
there is no access to production logs.
2026-04-13 16:01:19 -07:00
Brandon Thomas b7c8151d28 error utils (reusable) (#1355) 2026-04-13 17:47:06 -04:00
Brandon Thomas 640e392d6c observability into more unknown pages (#1354) 2026-04-13 17:35:54 -04:00
Brandon Thomas 45651103f0 kinovi credits check + alerting (#1352) 2026-04-12 22:33:31 -04:00
Brandon Thomas cba8e2c074 kinovi jobs - better logging (#1351) 2026-04-12 21:24:36 -04:00
Brandon Thomas 07d9d537dd page on image batch failures (#1350) 2026-04-12 19:11:24 -04:00
Brandon Thomas 5770534fc3 error messages on failure to download (#1349) 2026-04-12 18:40:00 -04:00
Brandon Thomas 6fe0909879 kinovi order polling threshold in hours (#1348) 2026-04-12 17:18:17 -04:00
Brandon Thomas 834f0b48a9 fal webhook fixes 2 (#1347) 2026-04-12 05:32:16 -04:00
Brandon Thomas d64944b1c3 artcraft 0.26 -> 0.27 (#1346) 2026-04-12 03:10:01 -04:00
Brandon Thomas 03edd5faf6 tauri cors fix (#1344) 2026-04-12 03:03:07 -04:00
Brandon Thomas 0ef57ff97e fal webhook error handling (#1343)
extractors for fal webhook data with tests
2026-04-12 02:04:22 -04:00
Brandon Thomas adda7483e8 fix billing pages (#1342) 2026-04-11 19:45:58 -04:00
Tanish Manku e77b946d57 fix brief No items yet flash after username fetch resolves (#1336)
setInitialLoading(true) and setUsername(name) are batched together so
React never produces a frame where username is set but initialLoading is
still false, which would fall through to the No items yet branch before
refreshGallery has a chance to set initialLoading(true) itself.
2026-04-10 08:11:59 -07:00
Brandon Thomas 3a6fad1812 bump artcraft 0.25 -> 0.26 (#1335) 2026-04-10 10:45:31 -04:00
Brandon Thomas 9e04990224 character tokens in omni api (#1333) 2026-04-10 10:25:31 -04:00
Brandon Thomas 885b68630a bump artcraft 0.24 -> 0.25 (#1332) 2026-04-10 10:15:04 -04:00
Brandon Thomas 9fee0b1c99 rename omni image params before widespread use (#1331) 2026-04-10 09:08:31 -04:00
Brandon Thomas 631b98f605 omni video handling + tests for seedream 1.5 (rest to follow) (#1330)
adjusts handling, and begins to add unit tests to ensure this works
2026-04-10 09:04:26 -04:00
Brandon Thomas 3334cf7b81 omni image tests (#1329)
This is a substantial number of tests to ensure we don't screw up parameters when migrating over to "omni image generation" from the distinct one-off, model-by-model endpoints.
2026-04-10 01:18:40 -04:00
Tanish Manku 50d8abcfb1 Fix gallery showing blank when session fetch fails or is slow (#1328)
- Remove useless forceFilterRef and its stale useEffect; use forceFilter
prop directly in JSX
- Show skeleton immediately while username is being fetched (!username
condition) instead of flashing "No items yet"
- Track username fetch failure with usernameError state and surface an
error message with a working Retry button
- Fix retry: setUsername("") was a no-op (same value → React bails out);
use usernameRetryCount in deps to actually re-trigger the fetch
- Guard error UI with allItems.length === 0 so stale items stay visible
if a retry fails mid-session
2026-04-09 19:11:02 -07:00
Tanish Mankuandbflatastic bce8d166e0 Feature: Multifile upload (#1294)
Notable behavior:

- Mixing supported and/or unsupported file types will only keep files of
file type that match the first supported file type in the list received
from browser (e.g. PNG, OBJ, EXE, JPG, OBJ order selection of files will
only show PNG, JPG for image upload)
- Removing files until only one is left will result in the sidebar
vanishing and only a single preview showing. More files can be added by
selecting multiple files again through the file selection button
- Failure to upload has been handled but not tested. It is expected any
failed uploads could be retried with a "retry failed" button. Any
individual fail can also be retried via a retry button upon hovering on
the exclamation mark by the failed file's name
- All multi-file uploads are queued async and likely wouldn't be ordered
as shown in the preview list (first-come-first-serve)
- Potentially deadlocks if a task never responds. Likely handled by
backend timeouts. This deadlock state is only partial and not lossy as
successfully uploaded files will appear in gallery even if the task is
cancelled. Cancellation can be attempted at any point during the upload.
Uploads shown in progress may be successful after cancellation depending
on the internal upload progress.
- TopBar upload images button now opens the image modal instead for
consistency and same-component use.

---------

Co-authored-by: bflatastic <bombayapirat@gmail.com>
2026-04-09 09:07:21 -07:00
frankywashereandClaude Opus 4.6 5fc27a5153 Fix blank screen crash and text selection loss in video prompt box (#1315)
## Summary
- **Fix blank screen crash** during video recreate flow: typing text,
adjusting time slider, typing again, then clicking the text box caused
an uncaught DOM exception in MentionTextarea's contentEditable caret
operations, which unmounted the entire React tree (no error boundaries
existed)
- **Fix text selection disappearing** on mouse release in
MentionTextarea: click handler and sync useEffect were destroying active
selections by rewriting innerHTML and restoring only a collapsed caret
- **Add React Error Boundary** around PromptBoxVideo as a safety net

## Changes
### MentionTextarea.tsx
- Wrap `getCaretOffset`, `setCaretOffset`, `scrollCaretIntoView`,
`handleInput`, sync `useEffect`, and `getOffsetRect` in try/catch to
prevent uncaught DOMExceptions
- Add null guard for `childNodes` iteration and `parentNode` access
- Skip `detectMention` in `handleClick` when user has active text
selection
- Skip innerHTML rewrite in sync useEffect when editor has selected text

### PromptBoxVideo.tsx
- Stabilize `characterNames` with `useMemo` to prevent `mentionColorMap`
from rebuilding every render
- Move empty characters array to module-level constant for stable
reference
- Add cleanup `useEffect` for `durationTimerRef` to prevent stale timer
after unmount

### New: PromptBoxErrorBoundary.tsx
- React Error Boundary with "Try again" recovery button

### ImageToVideo.tsx
- Wrap `<PromptBoxVideo>` with `<PromptBoxErrorBoundary>`

## Reproduction steps (original bug)
1. Click "Recreate" on a video
2. Type text into the prompt box
3. Adjust the time slider
4. Type text again
5. Adjust the time slider and click the text box → **UI goes blank**

## Test plan
- [x] Reproduced blank screen bug on Mac before fix
- [x] Verified fix: repeated recreation flow no longer causes blank
screen
- [x] Verified text selection persists after mouse release
- [x] Video generation with @mentions, reference images, and duration
changes works correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 09:03:59 -07:00
Tanish Manku 51e5db6b12 Text2Image carousel: cross-set navigation at arrow key boundaries (#1322)
Instead of looping within the image set, pressing Right on the last
image advances to the next set and Left on the first image goes to the
previous set, consistent with Up/Down behavior.
2026-04-08 23:46:19 -07:00
Brandon Thomas ff1ce82f66 transcode quicktime (#1318) 2026-04-08 10:27:14 -04:00
bflatastic ccea741443 fix safari mobile api calls/login not working since its third party 2026-04-08 14:45:29 +07:00
Brandon Thomas 2b9882d2d7 fix tests 2026-04-08 00:03:47 -04:00
bflatastic 55cab30dec comment out web generation access for now 2026-04-08 10:06:22 +07:00
Brandon Thomas a2f204b1be additional context for pages (#1317) 2026-04-07 19:07:49 -04:00
Brandon Thomas 12e4ff7a29 Omni Gen Video Models (#1316) 2026-04-07 17:05:52 -04:00
Brandon Thomas 8b73569238 omni image models (#1314) 2026-04-07 03:38:23 -04:00
Brandon Thomas badabe57eb fix inference-job 2026-04-07 00:49:18 -04:00
Brandon Thomas 2e62eef85b Bt cleanup web (#1313) 2026-04-07 00:43:21 -04:00
Brandon Thomas fd9f002615 avt cleanup (fix paging) (#1312) 2026-04-07 00:29:18 -04:00
Brandon Thomas a0e50067bb stop the session and session jobs alert pages by fixing error handling (#1310)
- was still getting dozens of pages from these two endpoints
- fixed the error chain so this is semantically correct
- improved code quality of legacy code/endpoints
2026-04-06 20:56:28 -04:00
421 changed files with 27943 additions and 2614 deletions
Generated
+9
View File
@@ -3876,6 +3876,13 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "error_utils"
version = "0.0.1"
dependencies = [
"regex",
]
[[package]]
name = "errors"
version = "0.0.1"
@@ -10172,6 +10179,7 @@ dependencies = [
"anyhow",
"bootstrap",
"bucket_paths",
"chrono",
"cloud_storage",
"concurrency",
"easyenv",
@@ -11233,6 +11241,7 @@ dependencies = [
"elasticsearch",
"elasticsearch_schema",
"enums",
"error_utils",
"errors",
"fal_client",
"ffmpeg_utils",
+2
View File
@@ -59,6 +59,7 @@ members = [
"crates/lib/data/primitives",
"crates/lib/data/web_base64",
"crates/lib/easyenv", # NB: Vendored version of https://github.com/echelon/easyenv.rs that we'll improve.
"crates/lib/error_utils",
"crates/lib/errors",
"crates/lib/ffmpeg_utils",
"crates/lib/files/file_to_base64_url",
@@ -236,6 +237,7 @@ cookie_store = { path = "crates/lib/cookie_store" }
crockford = { path = "crates/lib/data/crockford" }
easyenv = { path = "crates/lib/easyenv" }
enums = { path = "crates/schema/public/enums" }
error_utils = { path = "crates/lib/error_utils" }
errors = { path = "crates/lib/errors" }
fal_client = { path = "crates/api_clients/fal_client" }
ffmpeg_utils = { path = "crates/lib/ffmpeg_utils" }
@@ -28,7 +28,7 @@ pub struct GenerateKling16ProImageToVideoRequest {
pub duration: Option<GenerateKling16ProDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling16ProAspectRatio {
/// 16:9 aspect ratio
@@ -39,7 +39,7 @@ pub enum GenerateKling16ProAspectRatio {
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling16ProDuration {
FiveSeconds,
@@ -24,7 +24,7 @@ pub struct GenerateKling21MasterImageToVideoRequest {
pub duration: Option<GenerateKling21MasterDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21MasterAspectRatio {
/// 16:9 aspect ratio
@@ -35,7 +35,7 @@ pub enum GenerateKling21MasterAspectRatio {
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21MasterDuration {
FiveSeconds,
@@ -28,7 +28,7 @@ pub struct GenerateKling21ProImageToVideoRequest {
pub duration: Option<GenerateKling21ProDuration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21ProAspectRatio {
/// 16:9 aspect ratio
@@ -39,7 +39,7 @@ pub enum GenerateKling21ProAspectRatio {
Square,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateKling21ProDuration {
FiveSeconds,
@@ -31,14 +31,14 @@ pub struct GenerateSeedance10LiteImageToVideoRequest {
pub aspect_ratio: Option<GenerateSeedance10LiteAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10LiteResolution {
FourEightyP,
SevenTwentyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10LiteDuration {
FiveSeconds,
@@ -46,7 +46,7 @@ pub enum GenerateSeedance10LiteDuration {
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateSeedance10LiteAspectRatio {
Auto,
@@ -24,7 +24,7 @@ pub struct GenerateVeo2ImageToVideoRequest {
pub duration: Option<GenerateVeo2Duration>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo2AspectRatio {
Auto,
@@ -33,7 +33,7 @@ pub enum GenerateVeo2AspectRatio {
TallNineSixteen, // 9:16
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo2Duration {
FiveSeconds,
@@ -27,14 +27,14 @@ pub struct GenerateVeo3FastImageToVideoRequest {
pub generate_audio: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3FastResolution {
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3FastDuration {
EightSeconds,
@@ -37,7 +37,7 @@ pub struct GenerateVeo3ImageToVideoRequest {
// pub auto_fix: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3AspectRatio {
WideSixteenNine, // 16:9
@@ -45,14 +45,14 @@ pub enum GenerateVeo3AspectRatio {
Square, // 1:1
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3Resolution {
SevenTwentyP, // 720p
TenEightyP, // 1080p
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum GenerateVeo3Duration {
EightSeconds,
@@ -41,14 +41,14 @@ pub struct Kling2p5TurboProMultiFunctionVideoGenRequest {
pub aspect_ratio: Option<Kling2p5TurboProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p5TurboProMultiFunctionVideoGenDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p5TurboProMultiFunctionVideoGenAspectRatio {
Square,
@@ -45,14 +45,14 @@ pub struct Kling2p6ProMultiFunctionVideoGenRequest {
pub aspect_ratio: Option<Kling2p6ProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p6ProMultiFunctionVideoGenDuration {
FiveSeconds,
TenSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Kling2p6ProMultiFunctionVideoGenAspectRatio {
Square,
@@ -35,14 +35,14 @@ pub struct Sora2MultiFunctionVideoGenRequest {
pub aspect_ratio: Option<Sora2MultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2MultiFunctionVideoGenResolution {
Auto,
SevenTwentyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2MultiFunctionVideoGenDuration {
FourSeconds,
@@ -50,7 +50,7 @@ pub enum Sora2MultiFunctionVideoGenDuration {
TwelveSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2MultiFunctionVideoGenAspectRatio {
Auto,
@@ -35,7 +35,7 @@ pub struct Sora2ProMultiFunctionVideoGenRequest {
pub aspect_ratio: Option<Sora2ProMultiFunctionVideoGenAspectRatio>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2ProMultiFunctionVideoGenResolution {
Auto,
@@ -43,7 +43,7 @@ pub enum Sora2ProMultiFunctionVideoGenResolution {
TenEightyP,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2ProMultiFunctionVideoGenDuration {
FourSeconds,
@@ -51,7 +51,7 @@ pub enum Sora2ProMultiFunctionVideoGenDuration {
TwelveSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Sora2ProMultiFunctionVideoGenAspectRatio {
Auto,
@@ -60,7 +60,7 @@ pub struct Veo3p1FastMultiFunctionVideoGenRequest {
pub auto_fix: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1FastMultiFunctionVideoGenDuration {
FourSeconds,
@@ -68,7 +68,7 @@ pub enum Veo3p1FastMultiFunctionVideoGenDuration {
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
Auto,
@@ -76,7 +76,7 @@ pub enum Veo3p1FastMultiFunctionVideoGenAspectRatio {
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1FastMultiFunctionVideoGenResolution {
SevenTwentyP,
@@ -60,7 +60,7 @@ pub struct Veo3p1MultiFunctionVideoGenRequest {
pub auto_fix: Option<bool>,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1MultiFunctionVideoGenDuration {
FourSeconds,
@@ -68,7 +68,7 @@ pub enum Veo3p1MultiFunctionVideoGenDuration {
EightSeconds,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
Auto,
@@ -76,7 +76,7 @@ pub enum Veo3p1MultiFunctionVideoGenAspectRatio {
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Veo3p1MultiFunctionVideoGenResolution {
SevenTwentyP,
@@ -40,11 +40,11 @@ pub struct OmniGenImageCostAndGenerateRequest {
pub image_batch_count: Option<u16>,
/// Only for angle manipulation models.
pub horizontal_angle: Option<f64>,
pub adjust_horizontal_angle: Option<f64>,
/// Only for angle manipulation models.
pub vertical_angle: Option<f64>,
pub adjust_vertical_angle: Option<f64>,
/// Only for angle manipulation models.
pub zoom: Option<f64>,
pub adjust_zoom: Option<f64>,
}
@@ -5,6 +5,7 @@ use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::media_files::MediaFileToken;
/// Shared request body for both the video cost estimate and video generation endpoints.
@@ -39,6 +40,10 @@ pub struct OmniGenVideoCostAndGenerateRequest {
/// Reference audio (optional).
pub reference_audio_media_tokens: Option<Vec<MediaFileToken>>,
/// Optional character tokens to reference in the prompt.
/// Characters are referenced in prompts as @CharacterName.
pub reference_character_tokens: Option<Vec<CharacterToken>>,
/// The resolution to use.
pub resolution: Option<CommonResolution>,
@@ -103,6 +103,7 @@ pub struct OmniGenImageModelDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub resolution_default: Option<CommonResolution>,
#[serde(skip_serializing_if = "Option::is_none")]
pub quality_options: Option<Vec<CommonQuality>>,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -1,10 +1,13 @@
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation_provider::GenerationProvider;
use serde_derive::{Deserialize, Serialize};
use utoipa::{IntoParams, ToSchema};
/// Video model to default to if none is specified
const DEFAULT_VIDEO_MODEL : CommonVideoModel = CommonVideoModel::Seedance2p0;
/// Query string parameters for the video models endpoint.
#[derive(Deserialize, IntoParams, ToSchema)]
@@ -110,6 +113,12 @@ pub struct OmniGenVideoModelDetails {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio_references_max_total_duration_seconds: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_references_supported: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub character_references_max: Option<u16>,
#[serde(skip_serializing_if = "Option::is_none")]
pub show_generate_with_sound_toggle: Option<bool>,
@@ -159,7 +168,7 @@ pub struct OmniGenVideoModelDetails {
impl Default for OmniGenVideoModelDetails {
fn default() -> Self {
Self {
model: CommonVideoModel::Seedance2p0,
model: DEFAULT_VIDEO_MODEL,
full_name: None,
text_prompt_supported: None,
text_prompt_max_length: None,
@@ -176,6 +185,8 @@ impl Default for OmniGenVideoModelDetails {
audio_references_supported: None,
audio_references_max: None,
audio_references_max_total_duration_seconds: None,
character_references_supported: None,
character_references_max: None,
show_generate_with_sound_toggle: None,
aspect_ratio_options: None,
aspect_ratio_default: None,
@@ -36,7 +36,11 @@ pub enum CommonAspectRatio {
// Auto values that bake in resolution
// These are from the Seedream models
#[serde(rename = "auto_2k")]
Auto2k,
#[serde(rename = "auto_3k")]
Auto3k,
#[serde(rename = "auto_4k")]
Auto4k,
// Defined aspect ratios that bake in resolution
@@ -13,6 +13,8 @@ pub enum CommonImageModel {
FluxPro11,
#[serde(rename = "flux_pro_1p1_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1")]
GptImage1,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "nano_banana")]
@@ -0,0 +1,11 @@
use serde_derive::{Deserialize, Serialize};
/// Common quality levels you can specify when enqueuing a generation.
/// Not every model will use this; models that don't simply ignore it.
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum CommonQuality {
High,
Medium,
Low,
}
@@ -2,6 +2,7 @@ pub mod audio_list_ref;
pub mod character_list_ref;
pub mod common_aspect_ratio;
pub mod common_image_model;
pub mod common_quality;
pub mod common_resolution;
pub mod common_splat_model;
pub mod common_video_model;
@@ -43,6 +43,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -31,6 +31,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -46,6 +46,7 @@ mod tests {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
horizontal_angle: None,
vertical_angle: None,
@@ -6,8 +6,11 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
plan: &PlanArtcraftFluxPro11<'_>,
) -> ImageGenerationCostEstimate {
// NB(bt): Slightly more than 3 cents, weird megapixel maths. We can eat that cost for now.
let cost_per_image: u64 = 3;
// 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,
@@ -43,6 +46,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -60,10 +64,10 @@ mod tests {
#[test]
fn test_estimate_cost_usd_cents() {
// ~$0.03/image = 3 cents each (slightly more than 3¢ actual, rounded up)
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);
// $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);
}
}
@@ -43,6 +43,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -0,0 +1,239 @@
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,
}
}
#[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::GenerateImageRequest;
use tokens::tokens::media_files::MediaFileToken;
fn base() -> GenerateImageRequest<'static> {
GenerateImageRequest {
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 = GenerateImageRequest {
quality,
aspect_ratio,
image_batch_count: Some(batch),
..base()
};
request.build().unwrap().estimate_costs()
.cost_in_usd_cents.unwrap()
}
fn estimate_edit(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
num_input_images: usize,
) -> u64 {
let tokens: Vec<MediaFileToken> = (0..num_input_images)
.map(|i| MediaFileToken::new_from_str(&format!("mf_test{:028}", i)))
.collect();
let request = GenerateImageRequest {
quality,
aspect_ratio,
image_batch_count: Some(batch),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base()
};
request.build().unwrap().estimate_costs()
.cost_in_usd_cents.unwrap()
}
// ── Default quality (None → High) ───────────────────────────────────────
#[test]
fn default_quality_square_one_image_costs_17_cents() {
assert_eq!(estimate_text(None, None, 1), 17);
}
#[test]
fn default_quality_square_four_images_costs_68_cents() {
assert_eq!(estimate_text(None, None, 4), 68);
}
// ── Low quality (2¢/image regardless of size) ───────────────────────────
#[test]
fn low_square_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::Square), 1), 2);
}
#[test]
fn low_wide_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::WideSixteenByNine), 1), 2);
}
#[test]
fn low_tall_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Low), Some(CommonAspectRatio::TallNineBySixteen), 1), 2);
}
#[test]
fn low_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::Low), None, 4), 8);
}
// ── Medium quality (5¢ square, 7¢ wide/tall) ───────────────────────────
#[test]
fn medium_square_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 1), 5);
}
#[test]
fn medium_wide_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 1), 7);
}
#[test]
fn medium_tall_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::TallNineBySixteen), 1), 7);
}
#[test]
fn medium_square_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::Square), 4), 20);
}
#[test]
fn medium_wide_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 4), 28);
}
// ── High quality (17¢ square, 25¢ wide/tall) ───────────────────────────
#[test]
fn high_square_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
}
#[test]
fn high_wide_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 1), 25);
}
#[test]
fn high_tall_one_image() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::TallNineBySixteen), 1), 25);
}
#[test]
fn high_square_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 4), 68);
}
#[test]
fn high_wide_four_images() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::WideSixteenByNine), 4), 100);
}
// ── Edit mode: adds 2¢ per input image ─────────────────────────────────
#[test]
fn edit_one_input_adds_2_cents() {
// High square 1 output = 17¢, plus 1 input × 2¢ = 19¢
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 1), 19);
}
#[test]
fn edit_three_inputs_adds_6_cents() {
// High square 1 output = 17¢, plus 3 inputs × 2¢ = 23¢
assert_eq!(estimate_edit(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1, 3), 23);
}
#[test]
fn edit_five_inputs_adds_10_cents() {
// Medium wide 2 outputs = 7×2 = 14¢, plus 5 inputs × 2¢ = 24¢
assert_eq!(estimate_edit(Some(CommonQuality::Medium), Some(CommonAspectRatio::WideSixteenByNine), 2, 5), 24);
}
#[test]
fn edit_low_quality_with_inputs() {
// Low square 1 output = 2¢, plus 2 inputs × 2¢ = 6¢
assert_eq!(estimate_edit(Some(CommonQuality::Low), None, 1, 2), 6);
}
// ── Text mode (no input images) has no input cost ──────────────────────
#[test]
fn text_mode_no_input_cost() {
assert_eq!(estimate_text(Some(CommonQuality::High), Some(CommonAspectRatio::Square), 1), 17);
}
}
@@ -9,42 +9,33 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
plan: &PlanArtcraftGptImage1p5<'_>,
) -> ImageGenerationCostEstimate {
// Pricing varies by quality and image size. 1 credit = 1 USD cent.
// Low: $0.01/image (any size)
// Medium: $0.03/image (square/unset), $0.05/image (wide or tall)
// High: $0.13/image (square/unset), $0.20/image (wide or tall)
// Per fal docs (fal-ai/gpt-image-1.5 and fal-ai/gpt-image-1.5/edit):
//
// From Fal (text to image) -
// 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¢
//
// Your request will cost different amounts based on the number of images, quality, and size.
// Input text tokens: free (we don't charge for these).
//
// You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens.
// You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity.
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
// Default quality: High (when request.quality is None).
//
// From Fal (image to image) -
//
// Your request will cost different amounts based on the number of images, quality, and size.
//
// You will be charged $0.005 per 1,000 input text tokens. One word is roughly 4 tokens.
// You will be charged $0.008 per 1,000 input image tokens. One 1024x1024 image is roughly 135 tokens in low fidelity mode, or 3,050 tokens in high fidelity mode.
// You will be charged $0.010 per 1,000 output text tokens. The model will consume tokens reasoning about your prompt based on it's complexity.
// For low quality, you will be charged $0.009 for 1024x1024 or $0.013 for any other size per image.
// For medium quality, you will be charged $0.034 for 1024x1024, $0.051 for 1024x1536 and $0.050 for 1536x1024 per image.
// For high quality, you will be charged $0.133 for 1024x1024, $0.200 for 1024x1536 or $0.199 for 1536x1024 per image.
//
let cost_per_image: u64 = match plan.quality {
GptImage1p5MultiFunctionImageGenQuality::Low => 1,
GptImage1p5MultiFunctionImageGenQuality::Medium => match plan.image_size {
Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 3,
Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 5,
},
GptImage1p5MultiFunctionImageGenQuality::High => match plan.image_size {
Some(GptImage1p5MultiFunctionImageGenSize::Square) | None => 13,
Some(GptImage1p5MultiFunctionImageGenSize::Wide) | Some(GptImage1p5MultiFunctionImageGenSize::Tall) => 20,
},
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 {
@@ -70,11 +61,16 @@ pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
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::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16, aspect_ratio: Option<CommonAspectRatio>) -> u64 {
fn estimate(
quality: Option<CommonQuality>,
aspect_ratio: Option<CommonAspectRatio>,
batch: u16,
) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::GptImage1p5,
provider: Provider::Artcraft,
@@ -82,7 +78,8 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio,
image_batch_count: Some(image_batch_count),
quality,
image_batch_count: Some(batch),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
@@ -90,34 +87,140 @@ mod tests {
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")
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 test_estimate_cost_medium_square() {
// Default quality (Medium) + square/unset size = 3 cents each
assert_eq!(estimate_usd_cents(1, None), 3);
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::Square)), 3);
assert_eq!(estimate_usd_cents(2, Some(CommonAspectRatio::Square)), 6);
assert_eq!(estimate_usd_cents(3, Some(CommonAspectRatio::Square)), 9);
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::Square)), 12);
fn default_quality_unset_one_image() {
assert_eq!(estimate(None, None, 1), 14);
}
#[test]
fn test_estimate_cost_medium_wide() {
// Medium + wide = 5 cents each
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::WideSixteenByNine)), 5);
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::WideSixteenByNine)), 20);
fn default_quality_wide_one_image() {
assert_eq!(estimate(None, Some(CommonAspectRatio::WideSixteenByNine), 1), 20);
}
#[test]
fn test_estimate_cost_medium_tall() {
// Medium + tall = 5 cents each
assert_eq!(estimate_usd_cents(1, Some(CommonAspectRatio::TallNineBySixteen)), 5);
assert_eq!(estimate_usd_cents(4, Some(CommonAspectRatio::TallNineBySixteen)), 20);
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);
}
}
@@ -43,6 +43,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -8,12 +8,17 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
plan: &PlanArtcraftNanaBanana2<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.08/image at 0.5K, $0.15/image at 1K or 2K, $0.30/image at 4K.
// (Stored as USD cents.)
// 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) => 8,
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 30,
_ => 15, // OneK, TwoK, or None (default 1K)
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 {
@@ -51,6 +56,7 @@ mod tests {
image_inputs: None,
resolution,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -66,17 +72,125 @@ mod tests {
.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), 4), 60);
// ── Default (None → 1K → 8¢/image) ─────────────────────────────────────────
// 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);
#[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);
}
}
@@ -50,6 +50,7 @@ mod tests {
image_inputs: None,
resolution,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -46,6 +46,7 @@ mod tests {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -43,6 +43,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -43,6 +43,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -43,6 +43,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -2,6 +2,7 @@ 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_nano_banana;
pub mod estimate_image_cost_artcraft_nano_banana_2;
@@ -0,0 +1,21 @@
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,
}
}
@@ -0,0 +1,20 @@
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,
}
}
@@ -0,0 +1,245 @@
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,
}
}
#[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<'static> {
PlanFalGptImage1 {
prompt: Some("p"),
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<'static> {
let image_urls = (0..num_input_images)
.map(|i| format!("https://fake.example.com/img{}.png", i))
.collect();
PlanFalGptImage1 {
prompt: Some("p"),
image_urls,
image_size,
quality,
num_images,
}
}
fn cents_text(
quality: FalGptImage1Quality,
image_size: Option<FalGptImage1ImageSize>,
num_images: FalGptImage1NumImages,
) -> u64 {
estimate_image_cost_fal_gpt_image_1(&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);
}
}
@@ -0,0 +1,180 @@
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,
}
}
#[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<'static> {
PlanFalGptImage1p5 {
prompt: Some("p"),
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);
}
}
@@ -48,6 +48,7 @@ mod tests {
image_inputs: None,
resolution,
aspect_ratio: None,
quality: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -0,0 +1,19 @@
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,
}
}
@@ -0,0 +1,19 @@
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,
}
}
@@ -0,0 +1,19 @@
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,
}
}
@@ -1,5 +1,12 @@
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_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;
@@ -47,6 +47,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_flux_1_dev_image_request()
@@ -69,6 +70,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_1_dev_image_request()
@@ -94,6 +96,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a bear walking through an abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -37,7 +37,6 @@ 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::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_1_schnell_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
@@ -47,6 +46,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a man and woman walking through a cyberpunk city at night"),
..base_flux_1_schnell_image_request()
@@ -69,6 +69,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_1_schnell_image_request()
@@ -94,6 +95,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a man and woman walking through a futuristic city at evening"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -51,6 +51,7 @@ mod tests {
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
quality: None,
image_batch_count: Some(1),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
horizontal_angle: Some(30.0),
@@ -47,6 +47,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_flux_pro_1p1_image_request()
@@ -69,6 +70,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_pro_1p1_image_request()
@@ -94,6 +96,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a bear walking through an abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -47,6 +47,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_flux_pro_1p1_ultra_image_request()
@@ -69,6 +70,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_pro_1p1_ultra_image_request()
@@ -94,6 +96,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a t-rex walking through an abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -0,0 +1,147 @@
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 {
Some(image_inputs) => {
let request = GptImage1EditImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
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.map(|p| p.to_string()),
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::GenerateImageRequest;
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 = GenerateImageRequest {
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 = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..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 = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night"),
..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 = GenerateImageRequest {
prompt: Some("Change the background to a desert with cacti"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
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);
}
}
@@ -50,6 +50,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_nano_banana_image_request()
@@ -79,6 +80,7 @@ mod tests {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
quality: None,
image_batch_count: Some(1),
..base_nano_banana_image_request()
};
@@ -103,6 +105,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -138,6 +141,7 @@ mod tests {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_nano_banana_image_request()
@@ -49,6 +49,7 @@ mod tests {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_nano_banana_2_image_request()
@@ -51,6 +51,7 @@ mod tests {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_image_request()
@@ -74,6 +75,7 @@ mod tests {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_image_request()
@@ -106,6 +108,7 @@ mod tests {
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(1),
..base_image_request()
};
@@ -52,6 +52,7 @@ mod tests {
let tokens = vec![MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8")];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::SquareHd),
quality: None,
image_batch_count: Some(1),
prompt: None,
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
@@ -51,6 +51,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_seedream_4_image_request()
@@ -80,6 +81,7 @@ mod tests {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
quality: None,
image_batch_count: Some(1),
..base_seedream_4_image_request()
};
@@ -104,6 +106,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -139,6 +142,7 @@ mod tests {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
@@ -51,6 +51,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
..base_seedream_4p5_image_request()
@@ -80,6 +81,7 @@ mod tests {
prompt: Some("Change the background to a spooky haunted house"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto2k),
quality: None,
image_batch_count: Some(1),
..base_seedream_4p5_image_request()
};
@@ -104,6 +106,7 @@ mod tests {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
@@ -139,6 +142,7 @@ mod tests {
prompt: Some("Change the background to a spooky haunted house"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
quality: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
@@ -47,6 +47,7 @@ mod tests {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
quality: None,
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
..base_seedream_5_lite_image_request()
@@ -2,6 +2,7 @@ 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_nano_banana;
pub mod generate_image_artcraft_nano_banana_2;
@@ -0,0 +1,32 @@
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,
};
pub async fn execute_fal_flux_pro_1p1(
plan: &PlanFalFluxPro11<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = FluxPro11Args {
prompt: plan.prompt.unwrap_or(""),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_fal(),
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,
}))
}
@@ -0,0 +1,32 @@
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,
};
pub async fn execute_fal_flux_pro_1p1_ultra(
plan: &PlanFalFluxPro11Ultra<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let args = FluxPro11UltraArgs {
prompt: plan.prompt.unwrap_or(""),
aspect_ratio: plan.aspect_ratio,
num_images: plan.num_images.to_fal(),
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,
}))
}
@@ -0,0 +1,160 @@
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,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
enqueue_gpt_image_1_text_to_image_webhook, EnqueueGptImage1TextToImageArgs,
};
pub async fn execute_fal_gpt_image_1(
plan: &PlanFalGptImage1<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueGptImage1TextToImageArgs {
prompt: plan.prompt.unwrap_or(""),
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,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
} else {
let args = EnqueueGptImage1EditImageArgs {
prompt: plan.prompt.unwrap_or(""),
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,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1_edit_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,
}))
}
#[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::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::get_fal_client;
fn base_fal_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::GptImage1,
provider: Provider::Fal,
prompt: Some("a cat in space"),
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 = GenerateImageRequest {
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 = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..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 = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a horse walking through a cyberpunk city at night"),
..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 = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(1),
prompt: Some("change the background to a desert"),
..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);
}
}
@@ -0,0 +1,56 @@
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,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
enqueue_gpt_image_1p5_text_to_image_webhook, EnqueueGptImage1p5TextToImageArgs,
};
pub async fn execute_fal_gpt_image_1p5(
plan: &PlanFalGptImage1p5<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueGptImage1p5TextToImageArgs {
prompt: plan.prompt.unwrap_or(""),
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,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1p5_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
} else {
let args = EnqueueGptImage1p5EditImageArgs {
prompt: plan.prompt.unwrap_or(""),
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,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_gpt_image_1p5_image_edit_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,
}))
}
@@ -105,6 +105,7 @@ mod tests {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::TwoK),
quality: None,
image_batch_count: Some(1),
prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets"),
..base_fal_image_request()
@@ -129,6 +130,7 @@ mod tests {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
quality: None,
image_batch_count: Some(4),
prompt: Some("a golden retriever surfing a wave, cinematic, 4K"),
..base_fal_image_request()
@@ -161,6 +163,7 @@ mod tests {
image_inputs: Some(ImageListRef::Urls(&image_urls)),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
resolution: Some(CommonResolution::TwoK),
quality: None,
image_batch_count: Some(1),
..base_fal_image_request()
};
@@ -0,0 +1,50 @@
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,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
enqueue_bytedance_seedream_v4_text_to_image_webhook, EnqueueBytedanceSeedreamV4TextToImageArgs,
};
pub async fn execute_fal_seedream_4(
plan: &PlanFalSeedream4<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueBytedanceSeedreamV4TextToImageArgs {
prompt: plan.prompt.unwrap_or(""),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
} else {
let args = EnqueueBytedanceSeedreamV4EditImageArgs {
prompt: plan.prompt.unwrap_or(""),
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()),
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4_edit_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,
}))
}
@@ -0,0 +1,50 @@
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,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
enqueue_bytedance_seedream_v4p5_text_to_image_webhook, EnqueueBytedanceSeedreamV4p5TextToImageArgs,
};
pub async fn execute_fal_seedream_4p5(
plan: &PlanFalSeedream4p5<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueBytedanceSeedreamV4p5TextToImageArgs {
prompt: plan.prompt.unwrap_or(""),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4p5_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
} else {
let args = EnqueueBytedanceSeedreamV4p5EditImageArgs {
prompt: plan.prompt.unwrap_or(""),
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()),
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v4p5_edit_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,
}))
}
@@ -0,0 +1,50 @@
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,
};
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,
};
pub async fn execute_fal_seedream_5_lite(
plan: &PlanFalSeedream5Lite<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
let args = EnqueueBytedanceSeedreamV5LiteTextToImageArgs {
prompt: plan.prompt.unwrap_or(""),
num_images: Some(plan.num_images.to_t2i()),
max_images: None,
image_size: plan.image_size.map(|s| s.to_t2i()),
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v5_lite_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
} else {
let args = EnqueueBytedanceSeedreamV5LiteEditImageArgs {
prompt: plan.prompt.unwrap_or(""),
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()),
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_bytedance_seedream_v5_lite_edit_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,
}))
}
@@ -1,5 +1,12 @@
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_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,5 +1,6 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_quality::CommonQuality;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
@@ -13,6 +14,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
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_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;
@@ -23,11 +25,18 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
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_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;
#[derive(Debug)]
#[derive(Clone, Debug)]
pub struct GenerateImageRequest<'a> {
/// Which model to use.
pub model: CommonImageModel,
@@ -49,6 +58,9 @@ pub struct GenerateImageRequest<'a> {
/// The aspect ratio to use.
pub aspect_ratio: Option<CommonAspectRatio>,
/// The quality level for generation. Not all models use this.
pub quality: Option<CommonQuality>,
/// How many images to generate.
pub image_batch_count: Option<u16>,
@@ -90,6 +102,7 @@ impl<'a> GenerateImageRequest<'a> {
CommonImageModel::Flux1Schnell => plan_generate_image_artcraft_flux_1_schnell(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::NanoBanana => plan_generate_image_artcraft_nano_banana(self),
CommonImageModel::NanoBanana2 => plan_generate_image_artcraft_nano_banana_2(self),
@@ -106,6 +119,13 @@ impl<'a> GenerateImageRequest<'a> {
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::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),
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),
@@ -1,4 +1,5 @@
/// The result of estimating the cost of an image generation plan.
#[derive(Clone, Debug)]
pub struct ImageGenerationCostEstimate {
pub cost_in_credits: Option<u64>,
pub cost_in_usd_cents: Option<u64>,
@@ -4,6 +4,7 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
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_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;
@@ -15,6 +16,13 @@ use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraf
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_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;
@@ -22,6 +30,7 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
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_nano_banana::execute_artcraft_nano_banana;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_2::execute_artcraft_nano_banana_2;
@@ -33,6 +42,13 @@ use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_
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_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;
@@ -42,6 +58,7 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
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_nano_banana::PlanArtcraftNanaBanana;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
@@ -53,6 +70,13 @@ use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraf
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_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;
@@ -63,6 +87,7 @@ pub enum ImageGenerationPlan<'a> {
ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell<'a>),
ArtcraftFluxPro11(PlanArtcraftFluxPro11<'a>),
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra<'a>),
ArtcraftGptImage1(PlanArtcraftGptImage1<'a>),
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5<'a>),
ArtcraftNanaBanana(PlanArtcraftNanaBanana<'a>),
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2<'a>),
@@ -74,6 +99,13 @@ pub enum ImageGenerationPlan<'a> {
ArtcraftFlux2LoraAngles(PlanArtcraftFlux2LoraAngles<'a>),
FalFlux1Dev(PlanFalFlux1Dev<'a>),
FalFlux1Schnell(PlanFalFlux1Schnell<'a>),
FalFluxPro11(PlanFalFluxPro11<'a>),
FalFluxPro11Ultra(PlanFalFluxPro11Ultra<'a>),
FalGptImage1(PlanFalGptImage1<'a>),
FalGptImage1p5(PlanFalGptImage1p5<'a>),
FalSeedream4(PlanFalSeedream4<'a>),
FalSeedream4p5(PlanFalSeedream4p5<'a>),
FalSeedream5Lite(PlanFalSeedream5Lite<'a>),
FalNanoBanana(PlanFalNanoBanana<'a>),
FalNanaBanana2(PlanFalNanaBanana2<'a>),
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
@@ -101,6 +133,10 @@ impl<'a> ImageGenerationPlan<'a> {
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
@@ -145,6 +181,34 @@ impl<'a> ImageGenerationPlan<'a> {
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::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
@@ -174,6 +238,9 @@ impl<'a> ImageGenerationPlan<'a> {
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)
}
@@ -207,6 +274,27 @@ impl<'a> ImageGenerationPlan<'a> {
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::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)
}
@@ -2,6 +2,7 @@ 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_nano_banana;
pub mod plan_generate_image_artcraft_nano_banana_2;
@@ -268,6 +268,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
@@ -295,6 +296,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_dev_image_request()
@@ -312,6 +314,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
@@ -264,6 +264,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
@@ -291,6 +292,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_schnell_image_request()
@@ -308,6 +310,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
@@ -80,7 +80,7 @@ fn plan_image_size(
match aspect_ratio {
None => Ok(None),
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => {
Ok(Some(S::SquareHd))
}
@@ -224,6 +224,7 @@ mod tests {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_flux_2_lora_angles_image_request()
@@ -268,6 +268,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_image_request()
@@ -295,6 +296,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_pro_1p1_image_request()
@@ -312,6 +314,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_image_request()
@@ -271,6 +271,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_ultra_image_request()
@@ -298,6 +299,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_pro_1p1_ultra_image_request()
@@ -315,6 +317,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_ultra_image_request()
@@ -0,0 +1,537 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_quality::CommonQuality;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use artcraft_api_defs::generate::image::edit::gpt_image_1_edit_image::{
GptImage1EditImageImageQuality, GptImage1EditImageImageSize, GptImage1EditImageNumImages,
};
use artcraft_api_defs::generate::image::text::generate_gpt_image_1_text_to_image::{
GenerateGptImage1TextToImageImageQuality, GenerateGptImage1TextToImageImageSize,
GenerateGptImage1TextToImageNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
/// Quality is shared between t2i and edit (Auto / Low / Medium / High).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtcraftGptImage1Quality {
Auto,
Low,
Medium,
High,
}
/// Image size is shared between t2i and edit (Square / Horizontal / Vertical).
/// GPT Image 1 has no native auto size — None means "let the legacy handler
/// pick its default" (which is Square).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArtcraftGptImage1Size {
Square,
Horizontal,
Vertical,
}
#[derive(Debug, Clone, Copy)]
pub enum ArtcraftGptImage1NumImages {
One,
Two,
Three,
Four,
}
#[derive(Debug, Clone)]
pub struct PlanArtcraftGptImage1<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
/// Number of input images (0 for text-to-image). This is counted from either
/// media tokens or hydrated URLs, so the cost estimator can use it without
/// needing to inspect image_inputs (which is None for URL-form inputs).
pub num_input_images: u64,
pub image_size: Option<ArtcraftGptImage1Size>,
/// Quality defaults to High when the request leaves it unspecified, matching
/// the legacy storyteller-web handler defaults.
pub quality: ArtcraftGptImage1Quality,
pub num_images: ArtcraftGptImage1NumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_gpt_image_1<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let num_input_images = count_input_images(request.image_inputs);
let image_size = plan_image_size(request.aspect_ratio);
let quality = plan_quality(request.quality);
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::ArtcraftGptImage1(PlanArtcraftGptImage1 {
prompt: request.prompt,
image_inputs,
num_input_images,
image_size,
quality,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
}))
}
fn resolve_image_list_ref<'a>(
image_list_ref: Option<ImageListRef<'a>>,
) -> Result<Option<&'a Vec<MediaFileToken>>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
// Omni-gen distillation hydrates media tokens to URLs before running the
// Artcraft cost path. Cost only depends on quality + size + num_images +
// is_edit_mode (derived before this resolver runs), so URL-form inputs are
// accepted and dropped.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
fn count_input_images(image_inputs: Option<ImageListRef<'_>>) -> u64 {
match image_inputs {
None => 0,
Some(ImageListRef::MediaFileTokens(tokens)) => tokens.len() as u64,
Some(ImageListRef::Urls(urls)) => urls.len() as u64,
}
}
fn plan_quality(quality: Option<CommonQuality>) -> ArtcraftGptImage1Quality {
match quality {
Some(CommonQuality::Low) => ArtcraftGptImage1Quality::Low,
Some(CommonQuality::Medium) => ArtcraftGptImage1Quality::Medium,
Some(CommonQuality::High) => ArtcraftGptImage1Quality::High,
None => ArtcraftGptImage1Quality::High,
}
}
// GPT Image 1 image sizes: Square (1024x1024), Horizontal (1536x1024),
// Vertical (1024x1536). All 17 CommonAspectRatio variants map to one of these
// or None (let the legacy handler default to Square).
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<ArtcraftGptImage1Size> {
use ArtcraftGptImage1Size as S;
match aspect_ratio {
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto3k)
| Some(CommonAspectRatio::Auto4k) => None,
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
Some(CommonAspectRatio::WideThreeByTwo)
| Some(CommonAspectRatio::WideFourByThree)
| Some(CommonAspectRatio::WideFiveByFour)
| Some(CommonAspectRatio::WideSixteenByNine)
| Some(CommonAspectRatio::WideTwentyOneByNine)
| Some(CommonAspectRatio::Wide) => Some(S::Horizontal),
Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallFourByFive)
| Some(CommonAspectRatio::TallNineBySixteen)
| Some(CommonAspectRatio::TallNineByTwentyOne)
| Some(CommonAspectRatio::Tall) => Some(S::Vertical),
}
}
// GPT Image 1 supports 1, 2, 3, and 4 images.
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<ArtcraftGptImage1NumImages, ArtcraftRouterError> {
use ArtcraftGptImage1NumImages 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),
},
}
}
// ── Conversions to legacy storyteller-web request enums ────────────────────
impl ArtcraftGptImage1NumImages {
pub fn to_t2i(self) -> GenerateGptImage1TextToImageNumImages {
use GenerateGptImage1TextToImageNumImages as T;
match self {
Self::One => T::One,
Self::Two => T::Two,
Self::Three => T::Three,
Self::Four => T::Four,
}
}
pub fn to_edit(self) -> GptImage1EditImageNumImages {
use GptImage1EditImageNumImages as E;
match self {
Self::One => E::One,
Self::Two => E::Two,
Self::Three => E::Three,
Self::Four => E::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
impl ArtcraftGptImage1Quality {
pub fn to_t2i(self) -> GenerateGptImage1TextToImageImageQuality {
use GenerateGptImage1TextToImageImageQuality as T;
match self {
Self::Auto => T::Auto,
Self::Low => T::Low,
Self::Medium => T::Medium,
Self::High => T::High,
}
}
pub fn to_edit(self) -> GptImage1EditImageImageQuality {
use GptImage1EditImageImageQuality as E;
match self {
Self::Auto => E::Auto,
Self::Low => E::Low,
Self::Medium => E::Medium,
Self::High => E::High,
}
}
}
impl ArtcraftGptImage1Size {
pub fn to_t2i(self) -> GenerateGptImage1TextToImageImageSize {
use GenerateGptImage1TextToImageImageSize as T;
match self {
Self::Square => T::Square,
Self::Horizontal => T::Horizontal,
Self::Vertical => T::Vertical,
}
}
pub fn to_edit(self) -> GptImage1EditImageImageSize {
use GptImage1EditImageImageSize as E;
match self {
Self::Square => E::Square,
Self::Horizontal => E::Horizontal,
Self::Vertical => E::Vertical,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::test_helpers::base_gpt_image_1_image_request;
// ── Image size (aspect ratio mapping) ────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_auto_variants_yield_none() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar);
}
}
#[test]
fn image_size_square_variants() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(matches!(plan.image_size, Some(ArtcraftGptImage1Size::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn image_size_wide_variants_yield_horizontal() {
let wide_ars = [
CommonAspectRatio::WideThreeByTwo,
CommonAspectRatio::WideFourByThree,
CommonAspectRatio::WideFiveByFour,
CommonAspectRatio::WideSixteenByNine,
CommonAspectRatio::WideTwentyOneByNine,
CommonAspectRatio::Wide,
];
for ar in wide_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(matches!(plan.image_size, Some(ArtcraftGptImage1Size::Horizontal)), "expected Horizontal for {:?}", ar);
}
}
#[test]
fn image_size_tall_variants_yield_vertical() {
let tall_ars = [
CommonAspectRatio::TallTwoByThree,
CommonAspectRatio::TallThreeByFour,
CommonAspectRatio::TallFourByFive,
CommonAspectRatio::TallNineBySixteen,
CommonAspectRatio::TallNineByTwentyOne,
CommonAspectRatio::Tall,
];
for ar in tall_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(matches!(plan.image_size, Some(ArtcraftGptImage1Size::Vertical)), "expected Vertical for {:?}", ar);
}
}
// ── Quality default ──────────────────────────────────────────────────────
#[test]
fn quality_defaults_to_high() {
let request = base_gpt_image_1_image_request();
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(matches!(plan.quality, ArtcraftGptImage1Quality::High));
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1_image_request()
};
let result = request.build();
assert!(
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_default_is_one() {
let request = GenerateImageRequest { image_batch_count: None, ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(matches!(plan.num_images, ArtcraftGptImage1NumImages::One));
}
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, ArtcraftGptImage1NumImages::One),
(2, ArtcraftGptImage1NumImages::Two),
(3, ArtcraftGptImage1NumImages::Three),
(4, ArtcraftGptImage1NumImages::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
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 = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_gpt_image_1_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(9),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1_image_request()
};
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(
matches!(plan.num_images, ArtcraftGptImage1NumImages::Four),
"expected Four for count 9 with {:?}", strategy,
);
}
}
// ── Image inputs (mode detection) ────────────────────────────────────────
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(plan.image_inputs.is_none());
}
#[test]
fn media_token_image_inputs_is_edit_mode() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_gpt_image_1_image_request()
};
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(plan.image_inputs.is_some());
}
#[test]
fn url_image_inputs_are_accepted_for_cost_path() {
// The omni-gen distillation hydrates media tokens to URLs before running
// cost estimation against Artcraft. URLs are accepted (and dropped) since
// cost only depends on quality + size + num_images.
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_gpt_image_1_image_request()
};
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(plan.image_inputs.is_none());
}
// ── Idempotency token ────────────────────────────────────────────────────
#[test]
fn idempotency_token_is_generated_when_unset() {
let request = base_gpt_image_1_image_request();
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert!(!plan.idempotency_token.is_empty());
}
#[test]
fn idempotency_token_passthrough() {
let token = "11111111-1111-1111-1111-111111111111";
let request = GenerateImageRequest { idempotency_token: Some(token), ..base_gpt_image_1_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1")
};
assert_eq!(plan.idempotency_token, token);
}
// ── Conversions to legacy storyteller-web request enums ─────────────────
#[test]
fn num_images_to_t2i_round_trip() {
use GenerateGptImage1TextToImageNumImages as T;
assert!(matches!(ArtcraftGptImage1NumImages::One.to_t2i(), T::One));
assert!(matches!(ArtcraftGptImage1NumImages::Two.to_t2i(), T::Two));
assert!(matches!(ArtcraftGptImage1NumImages::Three.to_t2i(), T::Three));
assert!(matches!(ArtcraftGptImage1NumImages::Four.to_t2i(), T::Four));
}
#[test]
fn num_images_to_edit_round_trip() {
use GptImage1EditImageNumImages as E;
assert!(matches!(ArtcraftGptImage1NumImages::One.to_edit(), E::One));
assert!(matches!(ArtcraftGptImage1NumImages::Two.to_edit(), E::Two));
assert!(matches!(ArtcraftGptImage1NumImages::Three.to_edit(), E::Three));
assert!(matches!(ArtcraftGptImage1NumImages::Four.to_edit(), E::Four));
}
#[test]
fn quality_to_t2i_round_trip() {
use GenerateGptImage1TextToImageImageQuality as T;
assert!(matches!(ArtcraftGptImage1Quality::Auto.to_t2i(), T::Auto));
assert!(matches!(ArtcraftGptImage1Quality::Low.to_t2i(), T::Low));
assert!(matches!(ArtcraftGptImage1Quality::Medium.to_t2i(), T::Medium));
assert!(matches!(ArtcraftGptImage1Quality::High.to_t2i(), T::High));
}
#[test]
fn quality_to_edit_round_trip() {
use GptImage1EditImageImageQuality as E;
assert!(matches!(ArtcraftGptImage1Quality::Auto.to_edit(), E::Auto));
assert!(matches!(ArtcraftGptImage1Quality::Low.to_edit(), E::Low));
assert!(matches!(ArtcraftGptImage1Quality::Medium.to_edit(), E::Medium));
assert!(matches!(ArtcraftGptImage1Quality::High.to_edit(), E::High));
}
#[test]
fn size_to_t2i_round_trip() {
use GenerateGptImage1TextToImageImageSize as T;
assert!(matches!(ArtcraftGptImage1Size::Square.to_t2i(), T::Square));
assert!(matches!(ArtcraftGptImage1Size::Horizontal.to_t2i(), T::Horizontal));
assert!(matches!(ArtcraftGptImage1Size::Vertical.to_t2i(), T::Vertical));
}
#[test]
fn size_to_edit_round_trip() {
use GptImage1EditImageImageSize as E;
assert!(matches!(ArtcraftGptImage1Size::Square.to_edit(), E::Square));
assert!(matches!(ArtcraftGptImage1Size::Horizontal.to_edit(), E::Horizontal));
assert!(matches!(ArtcraftGptImage1Size::Vertical.to_edit(), E::Vertical));
}
}
@@ -1,4 +1,5 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_quality::CommonQuality;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
@@ -30,13 +31,14 @@ pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let quality = plan_quality(request.quality);
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::ArtcraftGptImage1p5(PlanArtcraftGptImage1p5 {
prompt: request.prompt,
image_inputs,
image_size,
quality: GptImage1p5MultiFunctionImageGenQuality::Medium,
quality,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
}))
@@ -48,9 +50,19 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// Omni-gen distillation hydrates media tokens to URLs before running the
// Artcraft cost path. Cost only depends on quality + size + num_images, so
// URL-form inputs are accepted and dropped.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
fn plan_quality(quality: Option<CommonQuality>) -> GptImage1p5MultiFunctionImageGenQuality {
match quality {
Some(CommonQuality::Low) => GptImage1p5MultiFunctionImageGenQuality::Low,
Some(CommonQuality::Medium) => GptImage1p5MultiFunctionImageGenQuality::Medium,
Some(CommonQuality::High) => GptImage1p5MultiFunctionImageGenQuality::High,
None => GptImage1p5MultiFunctionImageGenQuality::High,
}
}
@@ -67,6 +79,7 @@ fn plan_image_size(
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto3k)
| Some(CommonAspectRatio::Auto4k) => Ok(None),
// Square
@@ -195,10 +208,10 @@ mod tests {
// ── Quality ──────────────────────────────────────────────────────────────
#[test]
fn quality_defaults_to_medium() {
fn quality_defaults_to_high() {
let request = base_gpt_image_1p5_image_request();
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::Medium));
assert!(matches!(plan.quality, GptImage1p5MultiFunctionImageGenQuality::High));
}
// ── Num images ──────────────────────────────────────────────────────────────
@@ -277,17 +290,16 @@ mod tests {
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_gpt_image_1p5_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else {
panic!("expected ArtcraftGptImage1p5")
};
assert!(plan.image_inputs.is_none());
}
#[test]
@@ -45,9 +45,13 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// The omni-gen distillation path hydrates media tokens into URLs before
// running cost estimation against the Artcraft provider. The cost only
// depends on `num_images` and `is_edit_mode` (which is derived from
// `request.image_inputs.is_some()` before this resolver runs), so URL-form
// inputs are accepted here and dropped — there's no Artcraft execution
// path for Nano Banana that would need to read them back.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
@@ -290,6 +294,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_nano_banana_image_request()
@@ -323,6 +328,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_nano_banana_image_request()
@@ -341,6 +347,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_nano_banana_image_request()
@@ -356,17 +363,21 @@ mod tests {
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
fn url_image_inputs_are_accepted_for_cost_path() {
// The omni-gen distillation hydrates media tokens to URLs before running
// cost estimation against Artcraft. URLs are accepted (and dropped) since
// cost only depends on num_images + is_edit_mode.
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_nano_banana_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else {
panic!("expected ArtcraftNanaBanana")
};
// image_inputs is dropped (None), but edit-mode aspect-ratio handling
// still kicks in because is_edit_mode is derived from request.image_inputs.
assert!(plan.image_inputs.is_none());
}
#[test]
@@ -50,9 +50,12 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// The omni-gen distillation hydrates media tokens to URLs before running
// cost estimation against the Artcraft provider. Cost only depends on
// num_images + resolution + is_edit_mode (derived before this resolver
// runs), so URL-form inputs are accepted and dropped — there's no Artcraft
// execution path for Nano Banana 2 that would need to read them back.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
@@ -135,8 +138,10 @@ fn plan_resolution(
// No 3K option; default to 2K
Ok(Some(Nb2Res::TwoK))
}
// HalfK, FourEightyP, SevenTwentyP, TenEightyP — nearest supported is OneK
Some(R::HalfK | R::FourEightyP | R::SevenTwentyP | R::TenEightyP) => Ok(Some(Nb2Res::OneK)),
// HalfK is natively supported (and billed at the cheaper 0.5K rate).
Some(R::HalfK) => Ok(Some(Nb2Res::HalfK)),
// FourEightyP, SevenTwentyP, TenEightyP — nearest supported is OneK
Some(R::FourEightyP | R::SevenTwentyP | R::TenEightyP) => Ok(Some(Nb2Res::OneK)),
}
}
@@ -50,9 +50,12 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// The omni-gen distillation hydrates media tokens to URLs before running
// cost estimation against the Artcraft provider. Cost only depends on
// num_images + resolution + is_edit_mode (derived before this resolver
// runs), so URL-form inputs are accepted and dropped — there's no Artcraft
// execution path for Nano Banana Pro that would need to read them back.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
@@ -354,6 +357,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_image_request()
@@ -387,6 +391,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_image_request()
@@ -405,6 +410,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_image_request()
@@ -420,17 +426,19 @@ mod tests {
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
fn url_image_inputs_are_accepted_for_cost_path() {
// The omni-gen distillation hydrates media tokens to URLs before running
// cost estimation against Artcraft. URLs are accepted (and dropped) since
// cost only depends on num_images + resolution + is_edit_mode.
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else {
panic!("expected ArtcraftNanaBananaPro")
};
assert!(plan.image_inputs.is_none());
}
#[test]
@@ -82,7 +82,7 @@ fn plan_image_size(
match aspect_ratio {
None => Ok(None),
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto4k) => {
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) | Some(CommonAspectRatio::Auto4k) => {
Ok(Some(S::SquareHd))
}
@@ -226,6 +226,7 @@ mod tests {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(count),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_qwen_edit_2511_angles_image_request()
@@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// Omni-gen distillation hydrates media tokens to URLs before running the
// Artcraft cost path. Cost only depends on num_images, so URL-form inputs
// are accepted and dropped.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
@@ -71,7 +72,7 @@ fn plan_image_size(
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
// Square
@@ -295,6 +296,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
@@ -322,6 +324,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4_image_request()
@@ -336,6 +339,7 @@ mod tests {
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
@@ -348,16 +352,16 @@ mod tests {
// ── Image inputs ──────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_seedream_4_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else {
panic!("expected ArtcraftSeedream4")
};
assert!(plan.image_inputs.is_none());
}
#[test]
@@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// Omni-gen distillation hydrates media tokens to URLs before running the
// Artcraft cost path. Cost only depends on num_images, so URL-form inputs
// are accepted and dropped.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
@@ -71,7 +72,7 @@ fn plan_image_size(
if is_edit_mode { Ok(Some(S::Auto2k)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
// Square
@@ -296,6 +297,7 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
@@ -323,6 +325,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4p5_image_request()
@@ -337,6 +340,7 @@ mod tests {
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
@@ -349,16 +353,16 @@ mod tests {
// ── Image inputs ──────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
fn url_image_inputs_are_accepted_for_cost_path() {
let urls = vec!["https://example.com/image.jpg".to_string()];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_seedream_4p5_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
));
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else {
panic!("expected ArtcraftSeedream4p5")
};
assert!(plan.image_inputs.is_none());
}
#[test]
@@ -46,9 +46,10 @@ fn resolve_image_list_ref<'a>(
match image_list_ref {
None => Ok(None),
Some(ImageListRef::MediaFileTokens(tokens)) => Ok(Some(tokens)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
// Omni-gen distillation hydrates media tokens to URLs before running the
// Artcraft cost path. Cost only depends on num_images, so URL-form inputs
// are accepted and dropped.
Some(ImageListRef::Urls(_)) => Ok(None),
}
}
@@ -72,6 +73,7 @@ fn plan_image_size(
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto3k)),
// 5 Lite max is auto_3K; map Auto4k down to Auto3k
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)),
@@ -147,8 +149,6 @@ mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::test_helpers::base_seedream_5_lite_image_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
BytedanceSeedream5LiteMultiFunctionImageGenImageSize as S,
@@ -1,5 +1,12 @@
pub mod plan_generate_image_fal_flux_1_dev;
pub mod plan_generate_image_fal_flux_1_schnell;
pub mod plan_generate_image_fal_flux_pro_1p1;
pub mod plan_generate_image_fal_flux_pro_1p1_ultra;
pub mod plan_generate_image_fal_gpt_image_1;
pub mod plan_generate_image_fal_gpt_image_1p5;
pub mod plan_generate_image_fal_seedream_4;
pub mod plan_generate_image_fal_seedream_4p5;
pub mod plan_generate_image_fal_seedream_5_lite;
pub mod plan_generate_image_fal_nano_banana;
pub mod plan_generate_image_fal_nano_banana_2;
pub mod plan_generate_image_fal_nano_banana_pro;
@@ -0,0 +1,127 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_text_to_image_webhook::{
FluxPro11AspectRatio, FluxPro11NumImages,
};
#[derive(Debug, Clone, Copy)]
pub enum FalFluxPro11NumImages {
One,
Two,
Three,
Four,
}
#[derive(Debug, Clone)]
pub struct PlanFalFluxPro11<'a> {
pub prompt: Option<&'a str>,
pub aspect_ratio: FluxPro11AspectRatio,
pub num_images: FalFluxPro11NumImages,
}
pub fn plan_generate_image_fal_flux_pro_1p1<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
if request.image_inputs.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux Pro 1.1 is text-to-image only".to_string(),
}));
}
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalFluxPro11(PlanFalFluxPro11 {
prompt: request.prompt,
aspect_ratio,
num_images,
}))
}
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FluxPro11AspectRatio, ArtcraftRouterError> {
use FluxPro11AspectRatio as Ar;
match aspect_ratio {
None => Ok(Ar::Square),
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
Some(CommonAspectRatio::Square) => Ok(Ar::Square),
Some(CommonAspectRatio::SquareHd) => Ok(Ar::SquareHd),
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeSixteenByNine),
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineBySixteen),
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Ar::Square),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalFluxPro11NumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalFluxPro11NumImages::One),
2 => Ok(FalFluxPro11NumImages::Two),
3 => Ok(FalFluxPro11NumImages::Three),
4 => Ok(FalFluxPro11NumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalFluxPro11NumImages::Four),
},
}
}
impl FalFluxPro11NumImages {
pub fn to_fal(self) -> FluxPro11NumImages {
match self {
Self::One => FluxPro11NumImages::One,
Self::Two => FluxPro11NumImages::Two,
Self::Three => FluxPro11NumImages::Three,
Self::Four => FluxPro11NumImages::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
@@ -0,0 +1,126 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::text::enqueue_flux_pro_11_ultra_text_to_image_webhook::{
FluxPro11UltraAspectRatio, FluxPro11UltraNumImages,
};
#[derive(Debug, Clone, Copy)]
pub enum FalFluxPro11UltraNumImages {
One,
Two,
Three,
Four,
}
#[derive(Debug, Clone)]
pub struct PlanFalFluxPro11Ultra<'a> {
pub prompt: Option<&'a str>,
pub aspect_ratio: FluxPro11UltraAspectRatio,
pub num_images: FalFluxPro11UltraNumImages,
}
pub fn plan_generate_image_fal_flux_pro_1p1_ultra<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
if request.image_inputs.is_some() {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux Pro 1.1 Ultra is text-to-image only".to_string(),
}));
}
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalFluxPro11Ultra(PlanFalFluxPro11Ultra {
prompt: request.prompt,
aspect_ratio,
num_images,
}))
}
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FluxPro11UltraAspectRatio, ArtcraftRouterError> {
use FluxPro11UltraAspectRatio as Ar;
match aspect_ratio {
None => Ok(Ar::Square),
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Ar::Square),
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Ar::Square),
Some(CommonAspectRatio::WideFourByThree) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Ar::LandscapeThreeByTwo),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Ar::LandscapeSixteenByNine),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Ar::LandscapeTwentyOneByNine),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Ar::PortraitThreeByFour),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Ar::PortraitTwoByThree),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Ar::PortraitNineBySixteen),
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Ar::PortraitNineByTwentyOne),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Ar::LandscapeFourByThree),
Some(CommonAspectRatio::TallFourByFive) => Ok(Ar::PortraitThreeByFour),
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Ar::Square),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalFluxPro11UltraNumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalFluxPro11UltraNumImages::One),
2 => Ok(FalFluxPro11UltraNumImages::Two),
3 => Ok(FalFluxPro11UltraNumImages::Three),
4 => Ok(FalFluxPro11UltraNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalFluxPro11UltraNumImages::Four),
},
}
}
impl FalFluxPro11UltraNumImages {
pub fn to_fal(self) -> FluxPro11UltraNumImages {
match self {
Self::One => FluxPro11UltraNumImages::One,
Self::Two => FluxPro11UltraNumImages::Two,
Self::Three => FluxPro11UltraNumImages::Three,
Self::Four => FluxPro11UltraNumImages::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
@@ -0,0 +1,498 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_quality::CommonQuality;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1_edit_image_webhook::{
EnqueueGptImage1EditImageNumImages, EnqueueGptImage1EditImageQuality,
EnqueueGptImage1EditImageSize,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1_text_to_image_webhook::{
EnqueueGptImage1TextToImageNumImages, EnqueueGptImage1TextToImageQuality,
EnqueueGptImage1TextToImageSize,
};
#[derive(Debug, Clone, Copy)]
pub enum FalGptImage1NumImages {
One,
Two,
Three,
Four,
}
/// Quality is shared between t2i and edit (Low / Medium / High).
#[derive(Debug, Clone, Copy)]
pub enum FalGptImage1Quality {
Low,
Medium,
High,
}
/// GPT Image 1 supports three image sizes: 1024x1024, 1536x1024, 1024x1536.
#[derive(Debug, Clone, Copy)]
pub enum FalGptImage1ImageSize {
Square,
Horizontal,
Vertical,
}
#[derive(Debug, Clone)]
pub struct PlanFalGptImage1<'a> {
pub prompt: Option<&'a str>,
/// Image URLs for editing. Empty vec = text-to-image mode.
pub image_urls: Vec<String>,
pub image_size: Option<FalGptImage1ImageSize>,
/// Quality defaults to Medium when the request leaves it unspecified.
/// (Matches the fal cost calculator default.)
pub quality: FalGptImage1Quality,
pub num_images: FalGptImage1NumImages,
}
pub fn plan_generate_image_fal_gpt_image_1<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio);
let quality = plan_quality(request.quality);
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalGptImage1(PlanFalGptImage1 {
prompt: request.prompt,
image_urls,
image_size,
quality,
num_images,
}))
}
fn resolve_image_list_ref(
image_list_ref: Option<ImageListRef<'_>>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage1Quality {
match quality {
Some(CommonQuality::Low) => FalGptImage1Quality::Low,
Some(CommonQuality::Medium) => FalGptImage1Quality::Medium,
Some(CommonQuality::High) => FalGptImage1Quality::High,
None => FalGptImage1Quality::High,
}
}
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage1ImageSize> {
use FalGptImage1ImageSize as S;
match aspect_ratio {
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto3k)
| Some(CommonAspectRatio::Auto4k) => None,
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
Some(CommonAspectRatio::WideThreeByTwo)
| Some(CommonAspectRatio::WideFourByThree)
| Some(CommonAspectRatio::WideFiveByFour)
| Some(CommonAspectRatio::WideSixteenByNine)
| Some(CommonAspectRatio::WideTwentyOneByNine)
| Some(CommonAspectRatio::Wide) => Some(S::Horizontal),
Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallFourByFive)
| Some(CommonAspectRatio::TallNineBySixteen)
| Some(CommonAspectRatio::TallNineByTwentyOne)
| Some(CommonAspectRatio::Tall) => Some(S::Vertical),
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalGptImage1NumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalGptImage1NumImages::One),
2 => Ok(FalGptImage1NumImages::Two),
3 => Ok(FalGptImage1NumImages::Three),
4 => Ok(FalGptImage1NumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalGptImage1NumImages::Four),
},
}
}
// ── Conversions to fal-client enqueue enums ────────────────────────────────
impl FalGptImage1NumImages {
pub fn to_t2i(self) -> EnqueueGptImage1TextToImageNumImages {
use EnqueueGptImage1TextToImageNumImages as T;
match self {
Self::One => T::One,
Self::Two => T::Two,
Self::Three => T::Three,
Self::Four => T::Four,
}
}
pub fn to_edit(self) -> EnqueueGptImage1EditImageNumImages {
use EnqueueGptImage1EditImageNumImages as E;
match self {
Self::One => E::One,
Self::Two => E::Two,
Self::Three => E::Three,
Self::Four => E::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
impl FalGptImage1Quality {
pub fn to_t2i(self) -> EnqueueGptImage1TextToImageQuality {
use EnqueueGptImage1TextToImageQuality as T;
match self {
Self::Low => T::Low,
Self::Medium => T::Medium,
Self::High => T::High,
}
}
pub fn to_edit(self) -> EnqueueGptImage1EditImageQuality {
use EnqueueGptImage1EditImageQuality as E;
match self {
Self::Low => E::Low,
Self::Medium => E::Medium,
Self::High => E::High,
}
}
}
impl FalGptImage1ImageSize {
pub fn to_t2i(self) -> EnqueueGptImage1TextToImageSize {
use EnqueueGptImage1TextToImageSize as T;
match self {
Self::Square => T::Square,
Self::Horizontal => T::Horizontal,
Self::Vertical => T::Vertical,
}
}
pub fn to_edit(self) -> EnqueueGptImage1EditImageSize {
use EnqueueGptImage1EditImageSize as E;
match self {
Self::Square => E::Square,
Self::Horizontal => E::Horizontal,
Self::Vertical => E::Vertical,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
fn base_fal_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::GptImage1,
provider: Provider::Fal,
prompt: Some("a cat in space"),
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,
}
}
fn build_plan<'a>(request: &'a GenerateImageRequest<'a>) -> PlanFalGptImage1<'a> {
let ImageGenerationPlan::FalGptImage1(plan) =
plan_generate_image_fal_gpt_image_1(request).expect("plan should succeed")
else {
panic!("expected FalGptImage1 variant")
};
plan
}
// ── Mode detection ────────────────────────────────────────────────────────
#[test]
fn no_image_inputs_yields_empty_urls() {
let request = GenerateImageRequest { image_inputs: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_urls.is_empty());
}
#[test]
fn url_image_inputs_are_extracted() {
let urls = vec![
"https://example.com/a.jpg".to_string(),
"https://example.com/b.jpg".to_string(),
];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_fal_request()
};
let plan = build_plan(&request);
assert_eq!(plan.image_urls, urls);
}
#[test]
fn media_token_inputs_return_error() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_fal_request()
};
let result = plan_generate_image_fal_gpt_image_1(&request);
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
// ── Quality default ──────────────────────────────────────────────────────
#[test]
fn quality_defaults_to_high() {
let request = base_fal_request();
let plan = build_plan(&request);
assert!(matches!(plan.quality, FalGptImage1Quality::High));
}
// ── Image size mapping ───────────────────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_auto_variants_yield_none() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(auto_ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.image_size.is_none(), "expected None for {:?}", auto_ar);
}
}
#[test]
fn image_size_square_variants() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn image_size_wide_variants_yield_horizontal() {
let wide_ars = [
CommonAspectRatio::WideThreeByTwo,
CommonAspectRatio::WideFourByThree,
CommonAspectRatio::WideFiveByFour,
CommonAspectRatio::WideSixteenByNine,
CommonAspectRatio::WideTwentyOneByNine,
CommonAspectRatio::Wide,
];
for ar in wide_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Horizontal)), "expected Horizontal for {:?}", ar);
}
}
#[test]
fn image_size_tall_variants_yield_vertical() {
let tall_ars = [
CommonAspectRatio::TallTwoByThree,
CommonAspectRatio::TallThreeByFour,
CommonAspectRatio::TallFourByFive,
CommonAspectRatio::TallNineBySixteen,
CommonAspectRatio::TallNineByTwentyOne,
CommonAspectRatio::Tall,
];
for ar in tall_ars {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.image_size, Some(FalGptImage1ImageSize::Vertical)), "expected Vertical for {:?}", ar);
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
};
let result = plan_generate_image_fal_gpt_image_1(&request);
assert!(
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_default_is_one() {
let request = GenerateImageRequest { image_batch_count: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(matches!(plan.num_images, FalGptImage1NumImages::One));
}
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, FalGptImage1NumImages::One),
(2, FalGptImage1NumImages::Two),
(3, FalGptImage1NumImages::Three),
(4, FalGptImage1NumImages::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_fal_request() };
let plan = build_plan(&request);
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 = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_fal_request()
};
let result = plan_generate_image_fal_gpt_image_1(&request);
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(9),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
};
let plan = build_plan(&request);
assert!(
matches!(plan.num_images, FalGptImage1NumImages::Four),
"expected Four for count 9 with {:?}", strategy,
);
}
}
// ── as_u64 ───────────────────────────────────────────────────────────────
#[test]
fn num_images_as_u64() {
assert_eq!(FalGptImage1NumImages::One.as_u64(), 1);
assert_eq!(FalGptImage1NumImages::Two.as_u64(), 2);
assert_eq!(FalGptImage1NumImages::Three.as_u64(), 3);
assert_eq!(FalGptImage1NumImages::Four.as_u64(), 4);
}
// ── Conversions to fal-client enqueue enums ─────────────────────────────
#[test]
fn num_images_to_t2i_round_trip() {
use EnqueueGptImage1TextToImageNumImages as T;
assert!(matches!(FalGptImage1NumImages::One.to_t2i(), T::One));
assert!(matches!(FalGptImage1NumImages::Two.to_t2i(), T::Two));
assert!(matches!(FalGptImage1NumImages::Three.to_t2i(), T::Three));
assert!(matches!(FalGptImage1NumImages::Four.to_t2i(), T::Four));
}
#[test]
fn num_images_to_edit_round_trip() {
use EnqueueGptImage1EditImageNumImages as E;
assert!(matches!(FalGptImage1NumImages::One.to_edit(), E::One));
assert!(matches!(FalGptImage1NumImages::Two.to_edit(), E::Two));
assert!(matches!(FalGptImage1NumImages::Three.to_edit(), E::Three));
assert!(matches!(FalGptImage1NumImages::Four.to_edit(), E::Four));
}
#[test]
fn quality_to_t2i_round_trip() {
use EnqueueGptImage1TextToImageQuality as T;
assert!(matches!(FalGptImage1Quality::Low.to_t2i(), T::Low));
assert!(matches!(FalGptImage1Quality::Medium.to_t2i(), T::Medium));
assert!(matches!(FalGptImage1Quality::High.to_t2i(), T::High));
}
#[test]
fn quality_to_edit_round_trip() {
use EnqueueGptImage1EditImageQuality as E;
assert!(matches!(FalGptImage1Quality::Low.to_edit(), E::Low));
assert!(matches!(FalGptImage1Quality::Medium.to_edit(), E::Medium));
assert!(matches!(FalGptImage1Quality::High.to_edit(), E::High));
}
#[test]
fn size_to_t2i_round_trip() {
use EnqueueGptImage1TextToImageSize as T;
assert!(matches!(FalGptImage1ImageSize::Square.to_t2i(), T::Square));
assert!(matches!(FalGptImage1ImageSize::Horizontal.to_t2i(), T::Horizontal));
assert!(matches!(FalGptImage1ImageSize::Vertical.to_t2i(), T::Vertical));
}
#[test]
fn size_to_edit_round_trip() {
use EnqueueGptImage1EditImageSize as E;
assert!(matches!(FalGptImage1ImageSize::Square.to_edit(), E::Square));
assert!(matches!(FalGptImage1ImageSize::Horizontal.to_edit(), E::Horizontal));
assert!(matches!(FalGptImage1ImageSize::Vertical.to_edit(), E::Vertical));
}
}
@@ -0,0 +1,209 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_quality::CommonQuality;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_gpt_image_1p5_edit_image_webhook::{
EnqueueGptImage1p5EditImageNumImages, EnqueueGptImage1p5EditImageQuality,
EnqueueGptImage1p5EditImageSize,
};
use fal_client::requests::webhook::image::text::enqueue_gpt_image_1p5_text_to_image_webhook::{
EnqueueGptImage1p5TextToImageNumImages, EnqueueGptImage1p5TextToImageQuality,
EnqueueGptImage1p5TextToImageSize,
};
#[derive(Debug, Clone, Copy)]
pub enum FalGptImage1p5NumImages {
One,
Two,
Three,
Four,
}
/// Quality is shared between t2i and edit (Low / Medium / High).
#[derive(Debug, Clone, Copy)]
pub enum FalGptImage1p5Quality {
Low,
Medium,
High,
}
/// GPT Image 1.5 supports three image sizes: 1024x1024, 1536x1024, 1024x1536.
#[derive(Debug, Clone, Copy)]
pub enum FalGptImage1p5ImageSize {
Square,
Wide,
Tall,
}
#[derive(Debug, Clone)]
pub struct PlanFalGptImage1p5<'a> {
pub prompt: Option<&'a str>,
pub image_urls: Vec<String>,
pub image_size: Option<FalGptImage1p5ImageSize>,
pub quality: FalGptImage1p5Quality,
pub num_images: FalGptImage1p5NumImages,
}
pub fn plan_generate_image_fal_gpt_image_1p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio);
let quality = plan_quality(request.quality);
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalGptImage1p5(PlanFalGptImage1p5 {
prompt: request.prompt,
image_urls,
image_size,
quality,
num_images,
}))
}
fn resolve_image_list_ref(
image_list_ref: Option<ImageListRef<'_>>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
fn plan_quality(quality: Option<CommonQuality>) -> FalGptImage1p5Quality {
match quality {
Some(CommonQuality::Low) => FalGptImage1p5Quality::Low,
Some(CommonQuality::Medium) => FalGptImage1p5Quality::Medium,
Some(CommonQuality::High) => FalGptImage1p5Quality::High,
None => FalGptImage1p5Quality::High,
}
}
fn plan_image_size(aspect_ratio: Option<CommonAspectRatio>) -> Option<FalGptImage1p5ImageSize> {
use FalGptImage1p5ImageSize as S;
match aspect_ratio {
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto3k)
| Some(CommonAspectRatio::Auto4k) => None,
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Some(S::Square),
Some(CommonAspectRatio::WideThreeByTwo)
| Some(CommonAspectRatio::WideFourByThree)
| Some(CommonAspectRatio::WideFiveByFour)
| Some(CommonAspectRatio::WideSixteenByNine)
| Some(CommonAspectRatio::WideTwentyOneByNine)
| Some(CommonAspectRatio::Wide) => Some(S::Wide),
Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallFourByFive)
| Some(CommonAspectRatio::TallNineBySixteen)
| Some(CommonAspectRatio::TallNineByTwentyOne)
| Some(CommonAspectRatio::Tall) => Some(S::Tall),
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalGptImage1p5NumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalGptImage1p5NumImages::One),
2 => Ok(FalGptImage1p5NumImages::Two),
3 => Ok(FalGptImage1p5NumImages::Three),
4 => Ok(FalGptImage1p5NumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalGptImage1p5NumImages::Four),
},
}
}
impl FalGptImage1p5NumImages {
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageNumImages {
use EnqueueGptImage1p5TextToImageNumImages as T;
match self {
Self::One => T::One,
Self::Two => T::Two,
Self::Three => T::Three,
Self::Four => T::Four,
}
}
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageNumImages {
use EnqueueGptImage1p5EditImageNumImages as E;
match self {
Self::One => E::One,
Self::Two => E::Two,
Self::Three => E::Three,
Self::Four => E::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
impl FalGptImage1p5Quality {
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageQuality {
use EnqueueGptImage1p5TextToImageQuality as T;
match self {
Self::Low => T::Low,
Self::Medium => T::Medium,
Self::High => T::High,
}
}
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageQuality {
use EnqueueGptImage1p5EditImageQuality as E;
match self {
Self::Low => E::Low,
Self::Medium => E::Medium,
Self::High => E::High,
}
}
}
impl FalGptImage1p5ImageSize {
pub fn to_t2i(self) -> EnqueueGptImage1p5TextToImageSize {
use EnqueueGptImage1p5TextToImageSize as T;
match self {
Self::Square => T::Square,
Self::Wide => T::Wide,
Self::Tall => T::Tall,
}
}
pub fn to_edit(self) -> EnqueueGptImage1p5EditImageSize {
use EnqueueGptImage1p5EditImageSize as E;
match self {
Self::Square => E::Square,
Self::Wide => E::Wide,
Self::Tall => E::Tall,
}
}
}
@@ -256,6 +256,7 @@ mod tests {
image_inputs: None,
resolution: None,
aspect_ratio: None,
quality: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
@@ -478,7 +479,8 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
quality: None,
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
};
@@ -511,6 +513,7 @@ mod tests {
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_fal_request()
@@ -529,7 +532,8 @@ mod tests {
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
quality: None,
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
};
@@ -0,0 +1,209 @@
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::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4_edit_image_webhook::{
EnqueueBytedanceSeedreamV4EditImageNumImages, EnqueueBytedanceSeedreamV4EditImageSize,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4_text_to_image_webhook::{
EnqueueBytedanceSeedreamV4TextToImageNumImages, EnqueueBytedanceSeedreamV4TextToImageSize,
};
#[derive(Debug, Clone, Copy)]
pub enum FalSeedream4NumImages {
One,
Two,
Three,
Four,
}
/// Shared image-size shape between Seedream V4 t2i and edit (variants are identical).
#[derive(Debug, Clone, Copy)]
pub enum FalSeedream4ImageSize {
Square,
SquareHd,
PortraitFourThree,
PortraitSixteenNine,
LandscapeFourThree,
LandscapeSixteenNine,
Auto,
Auto2k,
Auto4k,
}
#[derive(Debug, Clone)]
pub struct PlanFalSeedream4<'a> {
pub prompt: Option<&'a str>,
/// Image URLs for editing. Empty vec = text-to-image mode.
pub image_urls: Vec<String>,
pub image_size: Option<FalSeedream4ImageSize>,
pub num_images: FalSeedream4NumImages,
}
pub fn plan_generate_image_fal_seedream_4<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_urls = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, is_edit_mode, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalSeedream4(PlanFalSeedream4 {
prompt: request.prompt,
image_urls,
image_size,
num_images,
}))
}
fn resolve_image_list_ref(
image_list_ref: Option<ImageListRef<'_>>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<FalSeedream4ImageSize>, ArtcraftRouterError> {
use FalSeedream4ImageSize as S;
match aspect_ratio {
None => Ok(None),
Some(CommonAspectRatio::Auto) => {
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Some(S::LandscapeSixteenNine)),
},
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Some(S::PortraitSixteenNine)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalSeedream4NumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalSeedream4NumImages::One),
2 => Ok(FalSeedream4NumImages::Two),
3 => Ok(FalSeedream4NumImages::Three),
4 => Ok(FalSeedream4NumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalSeedream4NumImages::Four),
},
}
}
impl FalSeedream4NumImages {
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4TextToImageNumImages {
use EnqueueBytedanceSeedreamV4TextToImageNumImages as T;
match self {
Self::One => T::One,
Self::Two => T::Two,
Self::Three => T::Three,
Self::Four => T::Four,
}
}
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4EditImageNumImages {
use EnqueueBytedanceSeedreamV4EditImageNumImages as E;
match self {
Self::One => E::One,
Self::Two => E::Two,
Self::Three => E::Three,
Self::Four => E::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
impl FalSeedream4ImageSize {
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4TextToImageSize {
use EnqueueBytedanceSeedreamV4TextToImageSize as T;
match self {
Self::Square => T::Square,
Self::SquareHd => T::SquareHd,
Self::PortraitFourThree => T::PortraitFourThree,
Self::PortraitSixteenNine => T::PortraitSixteenNine,
Self::LandscapeFourThree => T::LandscapeFourThree,
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
Self::Auto => T::Auto,
Self::Auto2k => T::Auto2k,
Self::Auto4k => T::Auto4k,
}
}
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4EditImageSize {
use EnqueueBytedanceSeedreamV4EditImageSize as E;
match self {
Self::Square => E::Square,
Self::SquareHd => E::SquareHd,
Self::PortraitFourThree => E::PortraitFourThree,
Self::PortraitSixteenNine => E::PortraitSixteenNine,
Self::LandscapeFourThree => E::LandscapeFourThree,
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
Self::Auto => E::Auto,
Self::Auto2k => E::Auto2k,
Self::Auto4k => E::Auto4k,
}
}
}
@@ -0,0 +1,201 @@
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::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v4p5_edit_image_webhook::{
EnqueueBytedanceSeedreamV4p5EditImageNumImages, EnqueueBytedanceSeedreamV4p5EditImageSize,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v4p5_text_to_image_webhook::{
EnqueueBytedanceSeedreamV4p5TextToImageNumImages, EnqueueBytedanceSeedreamV4p5TextToImageSize,
};
#[derive(Debug, Clone, Copy)]
pub enum FalSeedream4p5NumImages {
One,
Two,
Three,
Four,
}
/// Note: Seedream V4.5 lacks the bare "Auto" image_size; only Auto2k/Auto4k are supported.
#[derive(Debug, Clone, Copy)]
pub enum FalSeedream4p5ImageSize {
Square,
SquareHd,
PortraitFourThree,
PortraitSixteenNine,
LandscapeFourThree,
LandscapeSixteenNine,
Auto2k,
Auto4k,
}
#[derive(Debug, Clone)]
pub struct PlanFalSeedream4p5<'a> {
pub prompt: Option<&'a str>,
pub image_urls: Vec<String>,
pub image_size: Option<FalSeedream4p5ImageSize>,
pub num_images: FalSeedream4p5NumImages,
}
pub fn plan_generate_image_fal_seedream_4p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalSeedream4p5(PlanFalSeedream4p5 {
prompt: request.prompt,
image_urls,
image_size,
num_images,
}))
}
fn resolve_image_list_ref(
image_list_ref: Option<ImageListRef<'_>>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<FalSeedream4p5ImageSize>, ArtcraftRouterError> {
use FalSeedream4p5ImageSize as S;
match aspect_ratio {
None => Ok(None),
// No bare Auto for v4.5 — fall back to Auto2k.
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) | Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Some(S::LandscapeSixteenNine)),
},
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Some(S::PortraitSixteenNine)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalSeedream4p5NumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalSeedream4p5NumImages::One),
2 => Ok(FalSeedream4p5NumImages::Two),
3 => Ok(FalSeedream4p5NumImages::Three),
4 => Ok(FalSeedream4p5NumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalSeedream4p5NumImages::Four),
},
}
}
impl FalSeedream4p5NumImages {
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4p5TextToImageNumImages {
use EnqueueBytedanceSeedreamV4p5TextToImageNumImages as T;
match self {
Self::One => T::One,
Self::Two => T::Two,
Self::Three => T::Three,
Self::Four => T::Four,
}
}
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4p5EditImageNumImages {
use EnqueueBytedanceSeedreamV4p5EditImageNumImages as E;
match self {
Self::One => E::One,
Self::Two => E::Two,
Self::Three => E::Three,
Self::Four => E::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
impl FalSeedream4p5ImageSize {
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV4p5TextToImageSize {
use EnqueueBytedanceSeedreamV4p5TextToImageSize as T;
match self {
Self::Square => T::Square,
Self::SquareHd => T::SquareHd,
Self::PortraitFourThree => T::PortraitFourThree,
Self::PortraitSixteenNine => T::PortraitSixteenNine,
Self::LandscapeFourThree => T::LandscapeFourThree,
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
Self::Auto2k => T::Auto2k,
Self::Auto4k => T::Auto4k,
}
}
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV4p5EditImageSize {
use EnqueueBytedanceSeedreamV4p5EditImageSize as E;
match self {
Self::Square => E::Square,
Self::SquareHd => E::SquareHd,
Self::PortraitFourThree => E::PortraitFourThree,
Self::PortraitSixteenNine => E::PortraitSixteenNine,
Self::LandscapeFourThree => E::LandscapeFourThree,
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
Self::Auto2k => E::Auto2k,
Self::Auto4k => E::Auto4k,
}
}
}
@@ -0,0 +1,211 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use fal_client::requests::webhook::image::edit::enqueue_bytedance_seedream_v5_lite_edit_image_webhook::{
EnqueueBytedanceSeedreamV5LiteEditImageNumImages, EnqueueBytedanceSeedreamV5LiteEditImageSize,
};
use fal_client::requests::webhook::image::text::enqueue_bytedance_seedream_v5_lite_text_to_image_webhook::{
EnqueueBytedanceSeedreamV5LiteTextToImageNumImages, EnqueueBytedanceSeedreamV5LiteTextToImageSize,
};
#[derive(Debug, Clone, Copy)]
pub enum FalSeedream5LiteNumImages {
One,
Two,
Three,
Four,
}
/// Note: Seedream V5 Lite supports Auto2k and Auto3k (no 4K, no bare Auto).
#[derive(Debug, Clone, Copy)]
pub enum FalSeedream5LiteImageSize {
Square,
SquareHd,
PortraitFourThree,
PortraitSixteenNine,
LandscapeFourThree,
LandscapeSixteenNine,
Auto2k,
Auto3k,
}
#[derive(Debug, Clone)]
pub struct PlanFalSeedream5Lite<'a> {
pub prompt: Option<&'a str>,
pub image_urls: Vec<String>,
pub image_size: Option<FalSeedream5LiteImageSize>,
pub num_images: FalSeedream5LiteNumImages,
}
pub fn plan_generate_image_fal_seedream_5_lite<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<ImageGenerationPlan<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_urls = resolve_image_list_ref(request.image_inputs)?;
let image_size = plan_image_size(request.aspect_ratio, request.resolution, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(ImageGenerationPlan::FalSeedream5Lite(PlanFalSeedream5Lite {
prompt: request.prompt,
image_urls,
image_size,
num_images,
}))
}
fn resolve_image_list_ref(
image_list_ref: Option<ImageListRef<'_>>,
) -> Result<Vec<String>, ArtcraftRouterError> {
match image_list_ref {
None => Ok(vec![]),
Some(ImageListRef::Urls(urls)) => Ok(urls.clone()),
Some(ImageListRef::MediaFileTokens(_)) => {
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
}
}
}
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
resolution: Option<CommonResolution>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<FalSeedream5LiteImageSize>, ArtcraftRouterError> {
use FalSeedream5LiteImageSize as S;
// Resolution-based auto sizes take precedence when an explicit Auto* aspect ratio
// isn't specified but a resolution is — Seedream v5 lite has no 4K, fall back to 3K.
match aspect_ratio {
None => match resolution {
Some(CommonResolution::TwoK) => Ok(Some(S::Auto2k)),
Some(CommonResolution::ThreeK) | Some(CommonResolution::FourK) => Ok(Some(S::Auto3k)),
_ => Ok(None),
},
Some(CommonAspectRatio::Auto) | Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto3k) => Ok(Some(S::Auto3k)),
// No 4K — fall back to 3K.
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)),
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
Some(unsupported @ CommonAspectRatio::WideFiveByFour)
| Some(unsupported @ CommonAspectRatio::WideThreeByTwo)
| Some(unsupported @ CommonAspectRatio::WideTwentyOneByNine) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Some(S::LandscapeSixteenNine)),
},
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
_ => Ok(Some(S::PortraitSixteenNine)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalSeedream5LiteNumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalSeedream5LiteNumImages::One),
2 => Ok(FalSeedream5LiteNumImages::Two),
3 => Ok(FalSeedream5LiteNumImages::Three),
4 => Ok(FalSeedream5LiteNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
_ => Ok(FalSeedream5LiteNumImages::Four),
},
}
}
impl FalSeedream5LiteNumImages {
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV5LiteTextToImageNumImages {
use EnqueueBytedanceSeedreamV5LiteTextToImageNumImages as T;
match self {
Self::One => T::One,
Self::Two => T::Two,
Self::Three => T::Three,
Self::Four => T::Four,
}
}
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV5LiteEditImageNumImages {
use EnqueueBytedanceSeedreamV5LiteEditImageNumImages as E;
match self {
Self::One => E::One,
Self::Two => E::Two,
Self::Three => E::Three,
Self::Four => E::Four,
}
}
pub fn as_u64(self) -> u64 {
match self {
Self::One => 1,
Self::Two => 2,
Self::Three => 3,
Self::Four => 4,
}
}
}
impl FalSeedream5LiteImageSize {
pub fn to_t2i(self) -> EnqueueBytedanceSeedreamV5LiteTextToImageSize {
use EnqueueBytedanceSeedreamV5LiteTextToImageSize as T;
match self {
Self::Square => T::Square,
Self::SquareHd => T::SquareHd,
Self::PortraitFourThree => T::PortraitFourThree,
Self::PortraitSixteenNine => T::PortraitSixteenNine,
Self::LandscapeFourThree => T::LandscapeFourThree,
Self::LandscapeSixteenNine => T::LandscapeSixteenNine,
Self::Auto2k => T::Auto2k,
Self::Auto3k => T::Auto3k,
}
}
pub fn to_edit(self) -> EnqueueBytedanceSeedreamV5LiteEditImageSize {
use EnqueueBytedanceSeedreamV5LiteEditImageSize as E;
match self {
Self::Square => E::Square,
Self::SquareHd => E::SquareHd,
Self::PortraitFourThree => E::PortraitFourThree,
Self::PortraitSixteenNine => E::PortraitSixteenNine,
Self::LandscapeFourThree => E::LandscapeFourThree,
Self::LandscapeSixteenNine => E::LandscapeSixteenNine,
Self::Auto2k => E::Auto2k,
Self::Auto3k => E::Auto3k,
}
}
}
@@ -0,0 +1,18 @@
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_1_6_pro::PlanArtcraftKling16Pro;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_artcraft_kling_1_6_pro(
plan: &PlanArtcraftKling16Pro<'_>,
) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_v1p6_pro: 5s = $0.475 (rounded up to 48¢), 10s = $0.95 (95¢).
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 95 } else { 48 };
VideoGenerationCostEstimate {
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,
}
}
@@ -0,0 +1,18 @@
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_master::PlanArtcraftKling21Master;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_artcraft_kling_2_1_master(
plan: &PlanArtcraftKling21Master<'_>,
) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_v2p1_master: 5s = $1.40, 10s = $2.80.
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 280 } else { 140 };
VideoGenerationCostEstimate {
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,
}
}
@@ -0,0 +1,18 @@
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_kling_2_1_pro::PlanArtcraftKling21Pro;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub(crate) fn estimate_video_cost_artcraft_kling_2_1_pro(
plan: &PlanArtcraftKling21Pro<'_>,
) -> VideoGenerationCostEstimate {
// Mirrors fal_client kling_v2p1_pro: 5s = $0.45, 10s = $0.90.
let cost_in_usd_cents: u64 = if plan.is_ten_seconds() { 90 } else { 45 };
VideoGenerationCostEstimate {
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,
}
}

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