Compare commits

..
Author SHA1 Message Date
Brandon Thomas d961d88459 fix api 2026-02-25 06:22:53 +07:00
506 changed files with 3523 additions and 16864 deletions
-3
View File
@@ -68,9 +68,6 @@ __pycache__/
# We use diesel for DB migrations only. We don't care about its codgen.
_ignored_schema.rs
# Diesel lock file
.diesel_lock
# AI Chat Sidecar has its own schema managed by diesel, but we don't care for diesel codegen.
crates/programs/aichatbot-sidecar/_ignored_schema.rs
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE wallets\nSET\n banked_credits = ?,\n version = version + 1\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "01f6369e0ab7b0fa5b95b4595827dcf15897f3b50cce5b1dc421101b157b7008"
}
@@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "33aa8a1cf85bf1b8b68f4e2ab46541f25d7e8bb5eeb503c5ec14cb5e115f69ce"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nUPDATE wallet_ledger_entries\nSET\n is_refunded = TRUE,\n maybe_linked_refund_ledger_token = ?\nWHERE token = ?\nLIMIT 1\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6701a7adad65bc351e4600dad3ebf0f5dbdefa7dc9a0f890790cfaf368bf572d"
}
@@ -1,102 +0,0 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n wallet_token as `wallet_token: tokens::tokens::wallets::WalletToken`,\n entry_type,\n is_refunded as `is_refunded: bool`,\n credits_delta,\n banked_credits_before,\n banked_credits_after,\n monthly_credits_before,\n monthly_credits_after\nFROM wallet_ledger_entries\nWHERE token = ?\nLIMIT 1\nFOR UPDATE\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "wallet_token: tokens::tokens::wallets::WalletToken",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | MULTIPLE_KEY | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 1,
"name": "entry_type",
"type_info": {
"type": "VarString",
"flags": "NOT_NULL | BINARY | NO_DEFAULT_VALUE",
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 2,
"name": "is_refunded: bool",
"type_info": {
"type": "Tiny",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 1
}
},
{
"ordinal": 3,
"name": "credits_delta",
"type_info": {
"type": "Long",
"flags": "NOT_NULL",
"char_set": 63,
"max_size": 11
}
},
{
"ordinal": 4,
"name": "banked_credits_before",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 5,
"name": "banked_credits_after",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 6,
"name": "monthly_credits_before",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
},
{
"ordinal": 7,
"name": "monthly_credits_after",
"type_info": {
"type": "Long",
"flags": "NOT_NULL | UNSIGNED",
"char_set": 63,
"max_size": 10
}
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "a7365998006f4d40ad26efb42e6958a7aed689562a4c5b8c91cbe0033d0270e7"
}
@@ -1,6 +1,6 @@
{
"db_name": "MySQL",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query": "\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"describe": {
"columns": [
{
@@ -72,16 +72,6 @@
"char_set": 224,
"max_size": 128
}
},
{
"ordinal": 7,
"name": "maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken",
"type_info": {
"type": "VarString",
"flags": "BINARY",
"char_set": 224,
"max_size": 128
}
}
],
"parameters": {
@@ -94,9 +84,8 @@
true,
false,
false,
true,
true
]
},
"hash": "cb33fd483f0fef7ec2a1f5950218f312a19e38c7971b88db949ccaa05bf7b7c4"
"hash": "ca96942d8214edb84f163cf40131ceed873209e86718054e1502219fc81d2dc6"
}
@@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "\nINSERT INTO generic_inference_jobs\nSET\n token = ?,\n uuid_idempotency_token = ?,\n\n job_type = ?,\n\n maybe_external_third_party = ?,\n maybe_external_third_party_id = ?,\n\n product_category = ?,\n inference_category = ?,\n\n maybe_model_type = NULL,\n maybe_model_token = NULL,\n\n maybe_input_source_token = NULL,\n maybe_input_source_token_type = NULL,\n\n maybe_download_url = NULL,\n maybe_cover_image_media_file_token = NULL,\n\n maybe_prompt_token = ?,\n\n maybe_wallet_ledger_entry_token = ?,\n\n maybe_raw_inference_text = NULL,\n\n maybe_inference_args = ?,\n\n maybe_creator_user_token = ?,\n maybe_creator_anonymous_visitor_token = ?,\n creator_ip_address = ?,\n creator_set_visibility = ?,\n\n priority_level = 0,\n is_keepalive_required = FALSE,\n max_duration_seconds = 0,\n\n is_debug_request = FALSE,\n maybe_routing_tag = NULL,\n\n status = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 15
},
"nullable": []
},
"hash": "ee228ab4a079fde6b3683b6cde2a2384d3def27a5d09baa5caf00a521d5d2bcd"
}
Generated
+43 -69
View File
@@ -677,11 +677,10 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "artcraft"
version = "0.0.1"
version = "0.8.0"
dependencies = [
"anyhow",
"artcraft_api_defs",
"artcraft_client",
"artcraft_router",
"base64 0.22.1",
"build_metadata",
@@ -715,6 +714,7 @@ dependencies = [
"serde_json",
"serial_test",
"sqlite_tasks",
"storyteller_client",
"tauri",
"tauri-build",
"tauri-plugin-dialog",
@@ -751,48 +751,16 @@ dependencies = [
"workspace-build-acceleration",
]
[[package]]
name = "artcraft_client"
version = "0.0.1"
dependencies = [
"anyhow",
"artcraft_api_defs",
"backoff",
"chrono",
"cloudflare_errors",
"cookie 0.18.1",
"enums",
"enums_public",
"errors",
"idempotency",
"log",
"mimetypes",
"reqwest 0.12.23",
"serde",
"serde_derive",
"serde_json",
"tokens",
"tokio",
"url",
"url-builder",
"uuid 1.18.1",
"workspace-build-acceleration",
]
[[package]]
name = "artcraft_router"
version = "0.0.1"
dependencies = [
"anyhow",
"artcraft_api_defs",
"artcraft_client",
"fal_client",
"log",
"seedance2pro",
"serde",
"serde_derive",
"strum 0.28.0",
"test_data",
"storyteller_client",
"tokens",
"tokio",
"uuid 1.18.1",
@@ -1524,7 +1492,7 @@ dependencies = [
"bitflags 2.11.0",
"cexpr",
"clang-sys",
"itertools 0.13.0",
"itertools 0.11.0",
"proc-macro2",
"quote",
"regex",
@@ -3472,7 +3440,7 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330c60081dcc4c72131f8eb70510f1ac07223e5d4163db481a04a0befcffa412"
dependencies = [
"libloading 0.8.6",
"libloading 0.7.4",
]
[[package]]
@@ -3777,7 +3745,7 @@ dependencies = [
"serde_json",
"sqlx",
"storyteller_root",
"strum 0.26.2",
"strum",
"tokens",
"tokio",
"workspace-build-acceleration",
@@ -3939,7 +3907,7 @@ dependencies = [
"sqlx",
"sqlx-core",
"sqlx-mysql",
"strum 0.26.2",
"strum",
"toml 0.8.23",
"utoipa",
"workspace-build-acceleration",
@@ -3953,7 +3921,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"strum 0.26.2",
"strum",
"toml 0.8.23",
"utoipa",
"workspace-build-acceleration",
@@ -6502,7 +6470,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
dependencies = [
"cfg-if",
"windows-targets 0.52.6",
"windows-targets 0.48.5",
]
[[package]]
@@ -7067,7 +7035,7 @@ dependencies = [
"serde_json",
"sqlx",
"storyteller_root",
"strum 0.26.2",
"strum",
"tempdir",
"tokens",
"tokio",
@@ -11509,7 +11477,6 @@ dependencies = [
"actix_helpers",
"anyhow",
"artcraft_api_defs",
"artcraft_router",
"async-stripe 0.26.0",
"async-trait",
"base64 0.22.1",
@@ -11582,8 +11549,8 @@ dependencies = [
"server_environment",
"speculoos",
"sqlx",
"strum 0.26.2",
"strum_macros 0.26.4",
"strum",
"strum_macros",
"tempdir",
"tempfile",
"testing",
@@ -11606,6 +11573,34 @@ dependencies = [
"zip 0.6.6",
]
[[package]]
name = "storyteller_client"
version = "0.0.1"
dependencies = [
"anyhow",
"artcraft_api_defs",
"backoff",
"chrono",
"cloudflare_errors",
"cookie 0.18.1",
"enums",
"enums_public",
"errors",
"idempotency",
"log",
"mimetypes",
"reqwest 0.12.23",
"serde",
"serde_derive",
"serde_json",
"tokens",
"tokio",
"url",
"url-builder",
"uuid 1.18.1",
"workspace-build-acceleration",
]
[[package]]
name = "storyteller_root"
version = "0.0.1"
@@ -11668,16 +11663,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
dependencies = [
"strum_macros 0.26.4",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros 0.28.0",
"strum_macros",
]
[[package]]
@@ -11693,18 +11679,6 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "strum_macros"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "subprocess"
version = "0.2.9"
@@ -12701,7 +12675,7 @@ dependencies = [
"sqlx",
"sqlx-core",
"sqlx-mysql",
"strum 0.26.2",
"strum",
"utoipa",
"workspace-build-acceleration",
]
@@ -14051,7 +14025,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"strum 0.26.2",
"strum",
"workspace-build-acceleration",
]
+5 -6
View File
@@ -7,7 +7,7 @@ members = [
# API clients for external services
# These might be custom implementations or wrappers over other client libraries.
"crates/api_clients/artcraft/artcraft_api_defs", # TODO: api_clients/{artcraft}/{api_defs,client}
"crates/api_clients/artcraft/artcraft_client",
"crates/api_clients/artcraft/storyteller_client",
"crates/api_clients/cloud_storage",
"crates/api_clients/fal_client",
"crates/api_clients/google_drive_common",
@@ -72,7 +72,7 @@ members = [
"crates/lib/files/videos",
"crates/lib/http_headers",
"crates/lib/idempotency",
"crates/api_clients/artcraft/artcraft_router",
"crates/lib/artcraft_router",
"crates/lib/image_algorithms",
"crates/lib/jobs_common",
"crates/lib/jwt_light",
@@ -208,7 +208,7 @@ workspace-build-acceleration = { path = "crates/workspace-build-acceleration" }
# Setting paths here ensures we don't need to do so in every crate's Cargo.toml
# It makes refactoring much easier.
actix_artcraft = { path = "crates/lib/actix_artcraft" }
artcraft_router = { path = "crates/api_clients/artcraft/artcraft_router" }
artcraft_router = { path = "crates/lib/artcraft_router" }
actix_helpers = { path = "crates/lib/actix_helpers" }
artcraft_api_defs = { path = "crates/api_clients/artcraft/artcraft_api_defs" }
build_metadata = { path = "crates/lib/build_metadata" }
@@ -240,7 +240,7 @@ seedance2pro = { path = "crates/api_clients/seedance2pro" }
primitives = { path = "crates/lib/data/primitives" }
shared_service_components = { path = "crates/lib/shared_service_components" }
sqlite_tasks = { path = "crates/schema/database/sqlite_tasks" }
artcraft_client = { path = "crates/api_clients/artcraft/artcraft_client" }
storyteller_client = { path = "crates/api_clients/artcraft/storyteller_client" }
subprocess_common = { path = "crates/lib/subprocess_common" }
test_data = { path = "crates/lib/test_data" }
testing = { path = "crates/lib/testing" }
@@ -283,13 +283,12 @@ redis = { version = "1.0.2", features = ["r2d2", "tokio-rustls-comp", "tokio-com
regex = "1.12.3"
reqwest = { version = "0.12.23", default-features = false, features = [ "charset", "http2", "system-proxy", "json", "cookies", "rustls-tls", "gzip" ] }
reqwest-websocket = { version = "0.5.0" }
serde = { version = "1.0.219", features = ["derive"] }
serde = { version = "1.0.219", features = ["derive"] }
serde_derive = "1.0.219"
serde_json = "1.0.140"
serde_yaml = "0.9.34+deprecated"
sha2 = "0.11.0-rc.2"
sha3 = "0.10.8"
strum = { version = "0.28.0", features = ["derive"] }
tauri = { version = "2.8.3", features = ["unstable", "devtools"] } # NB: "devtools" prevents App Store distribution; "ubstable" is needed for window access
tauri-plugin-dialog = "2.3.3"
tauri-plugin-fs = "2.4.2"
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
DROP COLUMN
maybe_wallet_ledger_entry_token;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE generic_inference_jobs
ADD COLUMN
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE wallet_ledger_entries
DROP COLUMN
is_refunded;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE wallet_ledger_entries
ADD COLUMN
is_refunded BOOLEAN NOT NULL DEFAULT FALSE;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE wallet_ledger_entries
DROP COLUMN
maybe_linked_refund_ledger_token;
@@ -1,7 +0,0 @@
-- noinspection SqlDialectInspectionForFile
-- noinspection SqlNoDataSourceInspectionForFile
-- noinspection SqlResolveForFile
ALTER TABLE wallet_ledger_entries
ADD COLUMN
maybe_linked_refund_ledger_token VARCHAR(32) DEFAULT NULL;
@@ -182,9 +182,6 @@ CREATE TABLE generic_inference_jobs (
is_from_api_user BOOLEAN NOT NULL DEFAULT FALSE,
is_for_twitch BOOLEAN NOT NULL DEFAULT FALSE,
-- If credits were spent for this job, this references the corresponding wallet ledger entry.
maybe_wallet_ledger_entry_token VARCHAR(32) DEFAULT NULL,
-- ========== DEVELOPMENT AND DEBUGGING METADATA ==========
-- If true, the request gets a "debug" flag, which may do different
@@ -40,12 +40,6 @@ CREATE TABLE wallet_ledger_entries (
-- Balance of monthly credits after the event.
monthly_credits_after INTEGER UNSIGNED NOT NULL DEFAULT 0,
-- Whether this ledger entry has been refunded.
is_refunded BOOLEAN NOT NULL DEFAULT FALSE,
-- If this entry was refunded, this points to the refund ledger entry.
maybe_linked_refund_ledger_token VARCHAR(32) DEFAULT NULL,
-- ========== RECORD TIMESTAMPS ==========
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -1,116 +0,0 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_image_model::CommonImageModel;
use enums::common::generation::common_video_resolution::CommonVideoResolution;
use enums::common::generation_provider::GenerationProvider;
pub const ESTIMATE_IMAGE_COST_PATH: &str = "/v1/generate/cost_estimate/image";
/// Request body for the image cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateImageCostRequest {
/// The image model to estimate costs for.
pub model: CommonImageModel,
/// The provider to route the generation through.
pub provider: GenerationProvider,
/// The type of generation (determines whether input images are involved).
/// This is a tagged enum, so it looks like:
/// "generation_mode": {"type": "text_to_image"}
/// "generation_mode": {"type": "image_edit", "count": 2}
pub generation_mode: GenerationMode,
/// Optional aspect ratio.
pub aspect_ratio: Option<CommonAspectRatio>,
/// Optional resolution.
pub resolution: Option<CommonVideoResolution>,
/// Number of images to generate in parallel.
pub image_batch_count: Option<u16>,
}
/// Describes the type of image generation being requested.
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GenerationMode {
/// No input images — generate from prompt only.
TextToImage,
/// One or more input images are provided for editing/compositing.
ImageEdit { count: u32 },
}
/// Response body for the image cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateImageCostResponse {
pub success: bool,
/// Estimated cost in credits.
pub cost_in_credits: Option<u64>,
/// Estimated cost in USD cents.
pub cost_in_usd_cents: Option<u64>,
/// Whether the generation is free for this user/plan.
pub is_free: bool,
/// Whether the user has unlimited generations.
pub is_unlimited: bool,
/// Whether the user is rate limited.
pub is_rate_limited: bool,
/// Whether the output will have a watermark.
pub has_watermark: bool,
}
/// Error response for the image cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateImageCostError {
pub success: bool,
pub error_type: EstimateImageCostErrorType,
pub error_message: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum EstimateImageCostErrorType {
InvalidProviderForModel,
InvalidInput,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialization_text_to_image() {
let request = EstimateImageCostRequest {
model: CommonImageModel::NanaBananaPro,
provider: GenerationProvider::Artcraft,
generation_mode: GenerationMode::TextToImage,
aspect_ratio: None,
resolution: None,
image_batch_count: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_image\"}"));
}
#[test]
fn test_serialization_image_edit() {
let request = EstimateImageCostRequest {
model: CommonImageModel::NanaBananaPro,
provider: GenerationProvider::Artcraft,
generation_mode: GenerationMode::ImageEdit { count: 2 },
aspect_ratio: None,
resolution: None,
image_batch_count: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"image_edit\",\"count\":2}"));
}
}
@@ -1,122 +0,0 @@
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation::common_video_resolution::CommonVideoResolution;
use enums::common::generation_provider::GenerationProvider;
pub const ESTIMATE_VIDEO_COST_PATH: &str = "/v1/generate/cost_estimate/video";
/// Request body for the video cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateVideoCostRequest {
/// The video model to estimate costs for.
pub model: CommonVideoModel,
/// The provider to route the generation through.
pub provider: GenerationProvider,
/// The type of generation (determines how many input images are involved).
/// This is a tagged enum, so it looks like:
/// "generation_mode": {"type": "text_to_video"}
/// "generation_mode": {"type": "reference_image_to_video", "count": 1}
pub generation_mode: GenerationMode,
/// Optional aspect ratio.
pub aspect_ratio: Option<CommonAspectRatio>,
/// Optional resolution.
pub resolution: Option<CommonVideoResolution>,
/// Duration in seconds.
pub duration_seconds: Option<u16>,
/// Number of videos to generate in parallel.
pub video_batch_count: Option<u16>,
}
/// Describes the type of video generation being requested.
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GenerationMode {
TextToVideo,
StartFrameToVideo,
StartAndEndFrameToVideo,
ReferenceImageToVideo { count: u32 },
}
/// Response body for the video cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateVideoCostResponse {
pub success: bool,
/// Estimated cost in credits.
pub cost_in_credits: Option<u64>,
/// Estimated cost in USD cents.
pub cost_in_usd_cents: Option<u64>,
/// Whether the generation is free for this user/plan.
pub is_free: bool,
/// Whether the user has unlimited generations.
pub is_unlimited: bool,
/// Whether the user is rate limited.
pub is_rate_limited: bool,
/// Whether the output will have a watermark.
pub has_watermark: bool,
}
/// Error response for the video cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
pub struct EstimateVideoCostError {
pub success: bool,
pub error_type: EstimateVideoCostErrorType,
pub error_message: String,
}
#[derive(Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum EstimateVideoCostErrorType {
InvalidProviderForModel,
InvalidInput,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_serialization_tagging1() {
let request = EstimateVideoCostRequest {
model: CommonVideoModel::GrokVideo,
provider: GenerationProvider::Artcraft,
generation_mode: GenerationMode::TextToVideo,
aspect_ratio: None,
resolution: None,
duration_seconds: None,
video_batch_count: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"text_to_video\"}"));
//assert_eq!(serialized, "{}");
}
#[test]
fn test_serialization_tagging2() {
let request = EstimateVideoCostRequest {
model: CommonVideoModel::GrokVideo,
provider: GenerationProvider::Artcraft,
generation_mode: GenerationMode::ReferenceImageToVideo { count: 1 },
aspect_ratio: None,
resolution: None,
duration_seconds: None,
video_batch_count: None,
};
let serialized = serde_json::to_string(&request).unwrap();
assert!(serialized.contains("\"generation_mode\":{\"type\":\"reference_image_to_video\",\"count\":1}"));
}
}
@@ -1,2 +0,0 @@
pub mod estimate_image_cost;
pub mod estimate_video_cost;
@@ -1,68 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/bytedance_seedream_5_lite";
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedream5LiteMultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<BytedanceSeedream5LiteMultiFunctionImageGenNumImages>,
pub max_images: Option<BytedanceSeedream5LiteMultiFunctionImageGenMaxImages>,
pub image_size: Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedream5LiteMultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedream5LiteMultiFunctionImageGenMaxImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum BytedanceSeedream5LiteMultiFunctionImageGenImageSize {
// Square
Square,
SquareHd,
// Tall
PortraitFourThree,
PortraitSixteenNine,
// Wide
LandscapeFourThree,
LandscapeSixteenNine,
// Auto (NB: 5 Lite max is auto_3K, not auto_4K like V4.5)
Auto2k,
Auto3k,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct BytedanceSeedream5LiteMultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,7 +1,5 @@
pub mod bytedance_seedream_v4_multi_function_image_gen;
pub mod bytedance_seedream_v4p5_multi_function_image_gen;
pub mod bytedance_seedream_5_lite_multi_function_image_gen;
pub mod gpt_image_1p5_multi_function_image_gen;
pub mod nano_banana_2_multi_function_image_gen;
pub mod nano_banana_multi_function_image_gen;
pub mod nano_banana_pro_multi_function_image_gen;
@@ -1,74 +0,0 @@
use serde_derive::{Deserialize, Serialize};
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
use utoipa::ToSchema;
pub const NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH: &str = "/v1/generate/image/multi_function/nano_banana_2";
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct NanaBanana2MultiFunctionImageGenRequest {
/// Idempotency token to prevent duplicate requests.
pub uuid_idempotency_token: String,
/// Text prompt to generate the image from.
pub prompt: Option<String>,
/// Image media tokens to include in the editing context.
/// If present, we're doing image editing (image-to-image / image-editing)
/// If absent, we're doing image generation (text-to-image)
pub image_media_tokens: Option<Vec<MediaFileToken>>,
/// Number of images to generate. Default is one.
pub num_images: Option<NanaBanana2MultiFunctionImageGenNumImages>,
/// Resolution of the image to generate. Default is OneK (1K).
pub resolution: Option<NanaBanana2MultiFunctionImageGenImageResolution>,
/// Aspect ratio of the images to generate. Default is "1:1"
pub aspect_ratio: Option<NanaBanana2MultiFunctionImageGenAspectRatio>,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanaBanana2MultiFunctionImageGenNumImages {
One, // Default
Two,
Three,
Four,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanaBanana2MultiFunctionImageGenImageResolution {
HalfK,
OneK,
TwoK,
FourK,
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum NanaBanana2MultiFunctionImageGenAspectRatio {
// Auto (only for image editing)
Auto, // Default for image editing.
// Square
OneByOne, // Default for text-to-image
// Wide
FiveByFour,
FourByThree,
ThreeByTwo,
SixteenByNine,
TwentyOneByNine,
// Tall
FourByFive,
ThreeByFour,
TwoByThree,
NineBySixteen,
}
#[derive(Serialize, Deserialize, ToSchema)]
pub struct NanaBanana2MultiFunctionImageGenResponse {
pub success: bool,
pub inference_job_token: InferenceJobToken,
}
@@ -1,4 +1,3 @@
pub mod cost_estimate;
pub mod image;
pub mod object;
pub mod video;
@@ -1,20 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::{
EstimateImageCostRequest, EstimateImageCostResponse, ESTIMATE_IMAGE_COST_PATH,
};
pub async fn estimate_image_cost(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: EstimateImageCostRequest,
) -> Result<EstimateImageCostResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
ESTIMATE_IMAGE_COST_PATH,
maybe_creds,
request,
).await?)
}
@@ -1 +0,0 @@
pub mod estimate_image_cost;
@@ -1,2 +0,0 @@
pub mod image;
pub mod video;
@@ -1,20 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::cost_estimate::estimate_video_cost::{
EstimateVideoCostRequest, EstimateVideoCostResponse, ESTIMATE_VIDEO_COST_PATH,
};
pub async fn estimate_video_cost(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: EstimateVideoCostRequest,
) -> Result<EstimateVideoCostResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
ESTIMATE_VIDEO_COST_PATH,
maybe_creds,
request,
).await?)
}
@@ -1 +0,0 @@
pub mod estimate_video_cost;
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{BytedanceSeedream5LiteMultiFunctionImageGenRequest, BytedanceSeedream5LiteMultiFunctionImageGenResponse, BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn bytedance_seedream_5_lite_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: BytedanceSeedream5LiteMultiFunctionImageGenRequest,
) -> Result<BytedanceSeedream5LiteMultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
BYTEDANCE_SEEDREAM_5_LITE_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,18 +0,0 @@
use crate::credentials::storyteller_credential_set::StorytellerCredentialSet;
use crate::error::storyteller_error::StorytellerError;
use crate::utils::api_host::ApiHost;
use crate::utils::basic_json_post_request::basic_json_post_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{NanaBanana2MultiFunctionImageGenRequest, NanaBanana2MultiFunctionImageGenResponse, NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH};
pub async fn nano_banana_2_multi_function_image_gen(
api_host: &ApiHost,
maybe_creds: Option<&StorytellerCredentialSet>,
request: NanaBanana2MultiFunctionImageGenRequest,
) -> Result<NanaBanana2MultiFunctionImageGenResponse, StorytellerError> {
Ok(basic_json_post_request(
api_host,
NANO_BANANA_2_MULTI_FUNCTION_IMAGE_GEN_PATH,
maybe_creds,
request,
).await?)
}
@@ -1,30 +0,0 @@
use serde_derive::{Deserialize, Serialize};
/// Common image models supported by the router.
/// Not all models are available through all providers.
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CommonImageModel {
#[serde(rename = "flux_1_dev")]
Flux1Dev,
#[serde(rename = "flux_1_schnell")]
Flux1Schnell,
#[serde(rename = "flux_pro_1p1")]
FluxPro11,
#[serde(rename = "flux_pro_1p1_ultra")]
FluxPro11Ultra,
#[serde(rename = "gpt_image_1p5")]
GptImage1p5,
#[serde(rename = "nano_banana")]
NanaBanana,
#[serde(rename = "nano_banana_2")]
NanaBanana2,
#[serde(rename = "nano_banana_pro")]
NanaBananaPro,
#[serde(rename = "seedream_4")]
Seedream4,
#[serde(rename = "seedream_4p5")]
Seedream4p5,
#[serde(rename = "seedream_5_lite")]
Seedream5Lite,
}
@@ -1,12 +0,0 @@
/// Strategy for handling requests where the model only supports text-to-image
/// but the request includes image inputs (image editing mode).
#[derive(Copy, Clone, Debug)]
pub enum GenerationModeMismatchStrategy {
/// Abort the generation: return an error if image inputs are provided
/// for a text-to-image-only model.
AbortGeneration,
/// Proceed anyway, ignoring the image inputs for the generation.
GenerateAnyway,
}
@@ -1,28 +0,0 @@
use crate::client::multi_router_client::MultiRouterClient;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::client_error::{ClientError, ClientType};
pub enum RouterClient {
Multi(MultiRouterClient),
Artcraft(RouterArtcraftClient),
Fal(RouterFalClient),
}
impl RouterClient {
pub fn get_artcraft_client_ref(&self) -> Result<&RouterArtcraftClient, ClientError> {
match self {
RouterClient::Artcraft(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_artcraft_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Artcraft)),
}
}
pub fn get_fal_client_ref(&self) -> Result<&RouterFalClient, ClientError> {
match self {
RouterClient::Fal(client) => Ok(client),
RouterClient::Multi(multi) => multi.get_fal_client_ref(),
_ => Err(ClientError::ClientNotConfigured(ClientType::Fal)),
}
}
}
@@ -1,12 +0,0 @@
use fal_client::creds::fal_api_key::FalApiKey;
pub struct RouterFalClient {
pub(crate) api_key: FalApiKey,
pub(crate) webhook_url: String,
}
impl RouterFalClient {
pub fn new(api_key: FalApiKey, webhook_url: String) -> Self {
Self { api_key, webhook_url }
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
pub(crate) fn estimate_image_cost_artcraft_flux_1_dev(
plan: &PlanArtcraftFlux1Dev<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.02/image (2 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 2;
let num_images: u64 = match plan.num_images {
GenerateFlux1DevTextToImageNumImages::One => 1,
GenerateFlux1DevTextToImageNumImages::Two => 2,
GenerateFlux1DevTextToImageNumImages::Three => 3,
GenerateFlux1DevTextToImageNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::Flux1Dev,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.02/image = 2 cents each
assert_eq!(estimate_usd_cents(1), 2);
assert_eq!(estimate_usd_cents(2), 4);
assert_eq!(estimate_usd_cents(3), 6);
assert_eq!(estimate_usd_cents(4), 8);
}
}
@@ -1,45 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
pub(crate) fn estimate_image_cost_artcraft_flux_1_schnell(
_plan: &PlanArtcraftFlux1Schnell<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: not available
ImageGenerationCostEstimate {
cost_in_credits: None,
cost_in_usd_cents: None,
is_free: true,
is_unlimited: true,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
#[test]
fn test_estimate_cost_is_none() {
let request = GenerateImageRequest {
model: CommonImageModel::Flux1Schnell,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
let estimate = request.build()
.expect("build should succeed")
.estimate_costs();
assert!(estimate.cost_in_usd_cents.is_none());
assert!(estimate.cost_in_credits.is_none());
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1(
plan: &PlanArtcraftFluxPro11<'_>,
) -> ImageGenerationCostEstimate {
// NB(bt): Slightly more than 3 cents, weird megapixel maths. We can eat that cost for now.
let cost_per_image: u64 = 3;
let num_images: u64 = match plan.num_images {
GenerateFluxPro11TextToImageNumImages::One => 1,
GenerateFluxPro11TextToImageNumImages::Two => 2,
GenerateFluxPro11TextToImageNumImages::Three => 3,
GenerateFluxPro11TextToImageNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::FluxPro11,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// ~$0.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);
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
pub(crate) fn estimate_image_cost_artcraft_flux_pro_1p1_ultra(
plan: &PlanArtcraftFluxPro11Ultra<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.06/image (6 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 6;
let num_images: u64 = match plan.num_images {
GenerateFluxPro11UltraTextToImageNumImages::One => 1,
GenerateFluxPro11UltraTextToImageNumImages::Two => 2,
GenerateFluxPro11UltraTextToImageNumImages::Three => 3,
GenerateFluxPro11UltraTextToImageNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::FluxPro11Ultra,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.06/image = 6 cents each
assert_eq!(estimate_usd_cents(1), 6);
assert_eq!(estimate_usd_cents(2), 12);
assert_eq!(estimate_usd_cents(3), 18);
assert_eq!(estimate_usd_cents(4), 24);
}
}
@@ -1,120 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality,
GptImage1p5MultiFunctionImageGenSize,
};
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
pub(crate) fn estimate_image_cost_artcraft_gpt_image_1p5(
plan: &PlanArtcraftGptImage1p5<'_>,
) -> ImageGenerationCostEstimate {
// 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)
//
// From Fal (text 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.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.
//
// 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,
},
};
let num_images: u64 = match plan.num_images {
GptImage1p5MultiFunctionImageGenNumImages::One => 1,
GptImage1p5MultiFunctionImageGenNumImages::Two => 2,
GptImage1p5MultiFunctionImageGenNumImages::Three => 3,
GptImage1p5MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16, aspect_ratio: Option<CommonAspectRatio>) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::GptImage1p5,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_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);
}
#[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);
}
#[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);
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::NanoBananaMultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
pub(crate) fn estimate_image_cost_artcraft_nano_banana(
plan: &PlanArtcraftNanaBanana<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
NanoBananaMultiFunctionImageGenNumImages::One => 1,
NanoBananaMultiFunctionImageGenNumImages::Two => 2,
NanoBananaMultiFunctionImageGenNumImages::Three => 3,
NanoBananaMultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::NanaBanana,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,79 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
NanaBanana2MultiFunctionImageGenImageResolution, NanaBanana2MultiFunctionImageGenNumImages,
};
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
pub(crate) fn estimate_image_cost_artcraft_nano_banana_2(
plan: &PlanArtcraftNanaBanana2<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.08/image at 0.5K, $0.15/image at 1K or 2K, $0.30/image at 4K.
// (Stored as USD cents.)
let cost_per_image: u64 = match plan.resolution {
Some(NanaBanana2MultiFunctionImageGenImageResolution::HalfK) => 8,
Some(NanaBanana2MultiFunctionImageGenImageResolution::FourK) => 30,
_ => 15, // OneK, TwoK, or None (default 1K)
};
let num_images: u64 = match plan.num_images {
NanaBanana2MultiFunctionImageGenNumImages::One => 1,
NanaBanana2MultiFunctionImageGenNumImages::Two => 2,
NanaBanana2MultiFunctionImageGenNumImages::Three => 3,
NanaBanana2MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_resolution::CommonResolution;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::NanaBanana2,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// 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);
// 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);
}
}
@@ -1,80 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenImageResolution, NanoBananaProMultiFunctionImageGenNumImages,
};
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
pub(crate) fn estimate_image_cost_artcraft_nano_banana_pro(
plan: &PlanArtcraftNanaBananaPro<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
// (Stored as USD cents.)
let cost_per_image: u64 = match plan.resolution {
Some(NanoBananaProMultiFunctionImageGenImageResolution::FourK) => 30,
_ => 15,
};
let num_images: u64 = match plan.num_images {
NanoBananaProMultiFunctionImageGenNumImages::One => 1,
NanoBananaProMultiFunctionImageGenNumImages::Two => 2,
NanoBananaProMultiFunctionImageGenNumImages::Three => 3,
NanoBananaProMultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_resolution::CommonResolution;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::NanaBananaPro,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// 1K/2K: $0.15/image = 15 cents
assert_eq!(estimate_usd_cents(None, 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 45);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
// 4K: $0.30/image = 30 cents
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 120);
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::BytedanceSeedreamV4MultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
pub(crate) fn estimate_image_cost_artcraft_seedream_4(
plan: &PlanArtcraftSeedream4<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.03/image (3 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 3;
let num_images: u64 = match plan.num_images {
BytedanceSeedreamV4MultiFunctionImageGenNumImages::One => 1,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedreamV4MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::Seedream4,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.03/image = 3 cents each
assert_eq!(estimate_usd_cents(1), 3);
assert_eq!(estimate_usd_cents(2), 6);
assert_eq!(estimate_usd_cents(3), 9);
assert_eq!(estimate_usd_cents(4), 12);
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::BytedanceSeedreamV4p5MultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
pub(crate) fn estimate_image_cost_artcraft_seedream_4p5(
plan: &PlanArtcraftSeedream4p5<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::One => 1,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::Seedream4p5,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,66 +0,0 @@
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::BytedanceSeedream5LiteMultiFunctionImageGenNumImages;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
pub(crate) fn estimate_image_cost_artcraft_seedream_5_lite(
plan: &PlanArtcraftSeedream5Lite<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.04/image (4 cents). 1 credit = 1 USD cent.
let cost_per_image: u64 = 4;
let num_images: u64 = match plan.num_images {
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::One => 1,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Two => 2,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Three => 3,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::Seedream5Lite,
provider: Provider::Artcraft,
prompt: None,
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// $0.04/image = 4 cents each
assert_eq!(estimate_usd_cents(1), 4);
assert_eq!(estimate_usd_cents(2), 8);
assert_eq!(estimate_usd_cents(3), 12);
assert_eq!(estimate_usd_cents(4), 16);
}
}
@@ -1,11 +0,0 @@
pub mod estimate_image_cost_artcraft_flux_1_dev;
pub mod estimate_image_cost_artcraft_flux_1_schnell;
pub mod estimate_image_cost_artcraft_flux_pro_1p1;
pub mod estimate_image_cost_artcraft_flux_pro_1p1_ultra;
pub mod estimate_image_cost_artcraft_gpt_image_1p5;
pub mod estimate_image_cost_artcraft_nano_banana;
pub mod estimate_image_cost_artcraft_nano_banana_2;
pub mod estimate_image_cost_artcraft_nano_banana_pro;
pub mod estimate_image_cost_artcraft_seedream_4;
pub mod estimate_image_cost_artcraft_seedream_4p5;
pub mod estimate_image_cost_artcraft_seedream_5_lite;
@@ -1,78 +0,0 @@
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::{
FalNbpNumImages, FalNbpResolution, PlanFalNanaBananaPro,
};
pub(crate) fn estimate_image_cost_fal_nano_banana_pro(
plan: &PlanFalNanaBananaPro<'_>,
) -> ImageGenerationCostEstimate {
// Pricing: $0.15/image at 1K or 2K resolution, $0.30/image at 4K.
// (Stored as USD cents.)
let cost_per_image: u64 = match plan.resolution {
Some(FalNbpResolution::FourK) => 30,
_ => 15,
};
let num_images: u64 = match plan.num_images {
FalNbpNumImages::One => 1,
FalNbpNumImages::Two => 2,
FalNbpNumImages::Three => 3,
FalNbpNumImages::Four => 4,
};
let cost_in_usd_cents = cost_per_image * num_images;
ImageGenerationCostEstimate {
cost_in_credits: Some(cost_in_usd_cents),
cost_in_usd_cents: Some(cost_in_usd_cents),
is_free: false,
is_unlimited: false,
is_rate_limited: false,
has_watermark: false,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_resolution::CommonResolution;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
fn estimate_usd_cents(resolution: Option<CommonResolution>, image_batch_count: u16) -> u64 {
let request = GenerateImageRequest {
model: CommonImageModel::NanaBananaPro,
provider: Provider::Fal,
prompt: None,
image_inputs: None,
resolution,
aspect_ratio: None,
image_batch_count: Some(image_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// 1K/2K: $0.15/image = 15 cents
assert_eq!(estimate_usd_cents(None, 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::TwoK), 1), 15);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 2), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 3), 45);
assert_eq!(estimate_usd_cents(Some(CommonResolution::OneK), 4), 60);
// 4K: $0.30/image = 30 cents
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 1), 30);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 2), 60);
assert_eq!(estimate_usd_cents(Some(CommonResolution::FourK), 4), 120);
}
}
@@ -1 +0,0 @@
pub(crate) mod estimate_image_cost_fal_nano_banana_pro;
@@ -1,2 +0,0 @@
pub(crate) mod artcraft;
pub(crate) mod fal;
@@ -1,116 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::GenerateFlux1DevTextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_1_dev_text_to_image::generate_flux_1_dev_text_to_image;
pub async fn execute_artcraft_flux_1_dev(
plan: &PlanArtcraftFlux1Dev<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFlux1DevTextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_1_dev_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_1_dev_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_dev() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_flux_1_dev_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_dev_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_1_dev_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_1_dev_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a bear walking through an abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_1_dev_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,116 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::GenerateFlux1SchnellTextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_1_schnell_text_to_image::generate_flux_1_schnell_text_to_image;
pub async fn execute_artcraft_flux_1_schnell(
plan: &PlanArtcraftFlux1Schnell<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFlux1SchnellTextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_1_schnell_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::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;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_schnell() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a man and woman walking through a cyberpunk city at night"),
..base_flux_1_schnell_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_1_schnell_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_1_schnell_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_1_schnell_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a man and woman walking through a futuristic city at evening"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_1_schnell_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,116 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::GenerateFluxPro11TextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_pro_11_text_to_image::generate_flux_pro_11_text_to_image;
pub async fn execute_artcraft_flux_pro_1p1(
plan: &PlanArtcraftFluxPro11<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFluxPro11TextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_pro_11_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_pro_1p1_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_flux_pro_1p1_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_pro_1p1_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_pro_1p1_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a bear walking through an abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_pro_1p1_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,116 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::GenerateFluxPro11UltraTextToImageRequest;
use artcraft_client::endpoints::generate::image::text::generate_flux_pro_11_ultra_text_to_image::generate_flux_pro_11_ultra_text_to_image;
pub async fn execute_artcraft_flux_pro_1p1_ultra(
plan: &PlanArtcraftFluxPro11Ultra<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GenerateFluxPro11UltraTextToImageRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
aspect_ratio: plan.aspect_ratio,
num_images: Some(plan.num_images),
};
let response = generate_flux_pro_11_ultra_text_to_image(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_flux_pro_1p1_ultra_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_ultra() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_flux_pro_1p1_ultra_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_flux_pro_1p1_ultra_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_flux_pro_1p1_ultra_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_flux_pro_1p1_ultra_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a t-rex walking through an abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_flux_pro_1p1_ultra_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,164 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_1p5::PlanArtcraftGptImage1p5;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::GptImage1p5MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen_image::gpt_image_1p5_multi_function_image_gen;
pub async fn execute_artcraft_gpt_image_1p5(
plan: &PlanArtcraftGptImage1p5<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = GptImage1p5MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
mask_image_token: None,
num_images: Some(plan.num_images),
image_size: plan.image_size,
background: None,
quality: Some(plan.quality),
input_fidelity: None,
output_format: None,
};
let response = gpt_image_1p5_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_gpt_image_1p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_gpt_image_1p5() {
let client = get_artcraft_client();
let request = 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_1p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_gpt_image_1p5() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
prompt: Some("Change the background to a desert with cacti"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
image_batch_count: Some(1),
..base_gpt_image_1p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_gpt_image_1p5_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a bear walking through a abandoned desert city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_gpt_image_1p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_gpt_image_1p5_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
prompt: Some("Change the background to an alien world"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_gpt_image_1p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,159 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana::PlanArtcraftNanaBanana;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::NanoBananaMultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_multi_function_image_gen_image::nano_banana_multi_function_image_gen;
pub async fn execute_artcraft_nano_banana(
plan: &PlanArtcraftNanaBanana<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = NanoBananaMultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
num_images: Some(plan.num_images),
aspect_ratio: plan.aspect_ratio,
};
let response = nano_banana_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_nano_banana_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_nano_banana_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_nano_banana() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
image_batch_count: Some(1),
..base_nano_banana_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_nano_banana_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_nano_banana_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_nano_banana_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_nano_banana_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,67 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_2::PlanArtcraftNanaBanana2;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::NanaBanana2MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_2_multi_function_image_gen_image::nano_banana_2_multi_function_image_gen;
pub async fn execute_artcraft_nano_banana_2(
plan: &PlanArtcraftNanaBanana2<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = NanaBanana2MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
num_images: Some(plan.num_images),
resolution: plan.resolution,
aspect_ratio: plan.aspect_ratio,
};
let response = nano_banana_2_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_nano_banana_2_image_request, get_artcraft_client};
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_2() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_nano_banana_2_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,123 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::NanoBananaProMultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::nano_banana_pro_multi_function_image_gen_image::nano_banana_pro_multi_function_image_gen;
pub async fn execute_artcraft_nano_banana_pro(
plan: &PlanArtcraftNanaBananaPro<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = NanoBananaProMultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
num_images: Some(plan.num_images),
resolution: plan.resolution,
aspect_ratio: plan.aspect_ratio,
};
let response = nano_banana_pro_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_image_request, get_artcraft_client};
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_pro() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::OneK),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_nano_banana_pro_batch_four() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
image_batch_count: Some(4),
prompt: Some("a dog surfing a wave, cinematic"),
..base_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_nano_banana_pro() {
let client = get_artcraft_client();
// Reference images: the man to transplant, then the destination scene.
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
MediaFileToken::new_from_str("m_2220d5we3masng5hvzg5zz68ywbdqd"),
];
let request = GenerateImageRequest {
prompt: Some("Add the man in the first image into the fantasy scene in the second image. Keep his hoodie and attire, but match the vibe of the fantasy scene."),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
resolution: Some(CommonResolution::OneK),
image_batch_count: Some(1),
..base_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,160 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::BytedanceSeedreamV4MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen_image::bytedance_seedream_v4_multi_function_image_gen;
pub async fn execute_artcraft_seedream_4(
plan: &PlanArtcraftSeedream4<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedreamV4MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
num_images: Some(plan.num_images),
image_size: plan.image_size,
max_images: None,
};
let response = bytedance_seedream_v4_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_seedream_4_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_seedream_4() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto),
image_batch_count: Some(1),
..base_seedream_4_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_seedream_4_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a shiba walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_seedream_4_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
prompt: Some("Change the background to a sunny beach"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,160 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::BytedanceSeedreamV4p5MultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen_image::bytedance_seedream_v4p5_multi_function_image_gen;
pub async fn execute_artcraft_seedream_4p5(
plan: &PlanArtcraftSeedream4p5<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedreamV4p5MultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
num_images: Some(plan.num_images),
image_size: plan.image_size,
max_images: None,
};
let response = bytedance_seedream_v4p5_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_4p5_image_request, get_artcraft_client};
use strum::IntoEnumIterator;
use tokens::tokens::media_files::MediaFileToken;
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_4p5() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
..base_seedream_4p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_edit_image_seedream_4p5() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
let request = GenerateImageRequest {
prompt: Some("Change the background to a spooky haunted house"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(CommonAspectRatio::Auto2k),
image_batch_count: Some(1),
..base_seedream_4p5_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_text_to_image_seedream_4p5_all_aspect_ratios() {
let client = get_artcraft_client();
for ar in CommonAspectRatio::iter() {
println!("--- text-to-image aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
#[tokio::test]
#[ignore] // manually run — fires one real API request per aspect ratio variant (expensive)
async fn test_edit_image_seedream_4p5_all_aspect_ratios() {
let client = get_artcraft_client();
let image_tokens = vec![
MediaFileToken::new_from_str("m_r45apt2swza6wp8j2z10237t92kkr8"),
];
for ar in CommonAspectRatio::iter() {
println!("--- image-edit aspect ratio: {:?} ---", ar);
let request = GenerateImageRequest {
prompt: Some("Change the background to a spooky haunted house"),
image_inputs: Some(ImageListRef::MediaFileTokens(&image_tokens)),
aspect_ratio: Some(ar),
image_batch_count: Some(1),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayMoreUpgrade,
..base_seedream_4p5_image_request()
};
match request.build() {
Err(e) => println!(" plan error: {:?}", e),
Ok(plan) => match plan.generate_image(&client).await {
Ok(response) => {
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!(" job token: {:?}", payload.inference_job_token);
}
Err(e) => println!(" generate error: {:?}", e),
},
}
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the responses above.
}
}
@@ -1,65 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
ArtcraftImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::BytedanceSeedream5LiteMultiFunctionImageGenRequest;
use artcraft_client::endpoints::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen_image::bytedance_seedream_5_lite_multi_function_image_gen;
pub async fn execute_artcraft_seedream_5_lite(
plan: &PlanArtcraftSeedream5Lite<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let request = BytedanceSeedream5LiteMultiFunctionImageGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
image_media_tokens: plan.image_inputs.map(|tokens| tokens.to_owned()),
num_images: Some(plan.num_images),
image_size: plan.image_size,
max_images: None,
};
let response = bytedance_seedream_5_lite_multi_function_image_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateImageResponse::Artcraft(ArtcraftImageResponsePayload {
inference_job_token: response.inference_job_token,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_seedream_5_lite_image_request, get_artcraft_client};
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_image_seedream_5_lite() {
let client = get_artcraft_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
image_batch_count: Some(1),
prompt: Some("a corgi walking through a cyberpunk city at night"),
..base_seedream_5_lite_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,11 +0,0 @@
pub mod generate_image_artcraft_flux_1_dev;
pub mod generate_image_artcraft_flux_1_schnell;
pub mod generate_image_artcraft_flux_pro_1p1;
pub mod generate_image_artcraft_flux_pro_1p1_ultra;
pub mod generate_image_artcraft_gpt_image_1p5;
pub mod generate_image_artcraft_nano_banana;
pub mod generate_image_artcraft_nano_banana_2;
pub mod generate_image_artcraft_nano_banana_pro;
pub mod generate_image_artcraft_seedream_4;
pub mod generate_image_artcraft_seedream_4p5;
pub mod generate_image_artcraft_seedream_5_lite;
@@ -1,185 +0,0 @@
use crate::client::router_fal_client::RouterFalClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_image::generate_image_response::{
FalImageResponsePayload, GenerateImageResponse,
};
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::{
FalNbpNumImages, FalNbpResolution, PlanFalNanaBananaPro,
};
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::{
enqueue_nano_banana_pro_image_edit_webhook, EnqueueNanoBananaProEditImageArgs,
EnqueueNanoBananaProEditImageNumImages, EnqueueNanoBananaProEditImageResolution,
};
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::{
enqueue_nano_banana_pro_text_to_image_webhook, EnqueueNanoBananaProTextToImageArgs,
EnqueueNanoBananaProTextToImageNumImages, EnqueueNanoBananaProTextToImageResolution,
};
pub async fn execute_fal_nano_banana_pro(
plan: &PlanFalNanaBananaPro<'_>,
fal_client: &RouterFalClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
let webhook_response = if plan.image_urls.is_empty() {
// Text-to-image mode
let args = EnqueueNanoBananaProTextToImageArgs {
prompt: plan.prompt.unwrap_or(""),
num_images: to_t2i_num_images(plan.num_images),
resolution: plan.resolution.map(to_t2i_resolution),
aspect_ratio: plan.t2i_aspect_ratio,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_nano_banana_pro_text_to_image_webhook(args)
.await
.map_err(|e| ArtcraftRouterError::Provider(ProviderError::Fal(e)))?
} else {
// Image-edit mode
let args = EnqueueNanoBananaProEditImageArgs {
prompt: plan.prompt.unwrap_or(""),
image_urls: plan.image_urls.clone(),
num_images: to_edit_num_images(plan.num_images),
resolution: plan.resolution.map(to_edit_resolution),
aspect_ratio: plan.edit_aspect_ratio,
webhook_url: fal_client.webhook_url.as_str(),
api_key: &fal_client.api_key,
};
enqueue_nano_banana_pro_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,
}))
}
fn to_t2i_num_images(n: FalNbpNumImages) -> EnqueueNanoBananaProTextToImageNumImages {
match n {
FalNbpNumImages::One => EnqueueNanoBananaProTextToImageNumImages::One,
FalNbpNumImages::Two => EnqueueNanoBananaProTextToImageNumImages::Two,
FalNbpNumImages::Three => EnqueueNanoBananaProTextToImageNumImages::Three,
FalNbpNumImages::Four => EnqueueNanoBananaProTextToImageNumImages::Four,
}
}
fn to_edit_num_images(n: FalNbpNumImages) -> EnqueueNanoBananaProEditImageNumImages {
match n {
FalNbpNumImages::One => EnqueueNanoBananaProEditImageNumImages::One,
FalNbpNumImages::Two => EnqueueNanoBananaProEditImageNumImages::Two,
FalNbpNumImages::Three => EnqueueNanoBananaProEditImageNumImages::Three,
FalNbpNumImages::Four => EnqueueNanoBananaProEditImageNumImages::Four,
}
}
fn to_t2i_resolution(r: FalNbpResolution) -> EnqueueNanoBananaProTextToImageResolution {
match r {
FalNbpResolution::OneK => EnqueueNanoBananaProTextToImageResolution::OneK,
FalNbpResolution::TwoK => EnqueueNanoBananaProTextToImageResolution::TwoK,
FalNbpResolution::FourK => EnqueueNanoBananaProTextToImageResolution::FourK,
}
}
fn to_edit_resolution(r: FalNbpResolution) -> EnqueueNanoBananaProEditImageResolution {
match r {
FalNbpResolution::OneK => EnqueueNanoBananaProEditImageResolution::OneK,
FalNbpResolution::TwoK => EnqueueNanoBananaProEditImageResolution::TwoK,
FalNbpResolution::FourK => EnqueueNanoBananaProEditImageResolution::FourK,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::{base_fal_image_request, get_fal_client};
use test_data::web::image_urls::{GHOST_IMAGE_URL, TREX_SKELETON_IMAGE_URL};
#[tokio::test]
#[ignore] // manually run — fires a real Fal API request
async fn test_text_to_image_fal_nano_banana_pro() {
let client = get_fal_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
resolution: Some(CommonResolution::TwoK),
image_batch_count: Some(1),
prompt: Some("a cyberpunk city skyline at dusk, neon lights reflecting on rain-soaked streets"),
..base_fal_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request ID: {:?}", payload.request_id);
println!("Gateway request ID: {:?}", payload.gateway_request_id);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real Fal API request
async fn test_text_to_image_fal_nano_banana_pro_batch_four() {
let client = get_fal_client();
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
resolution: Some(CommonResolution::OneK),
image_batch_count: Some(4),
prompt: Some("a golden retriever surfing a wave, cinematic, 4K"),
..base_fal_image_request()
};
let plan = request.build().unwrap();
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request ID: {:?}", payload.request_id);
println!("Gateway request ID: {:?}", payload.gateway_request_id);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real Fal API request
async fn test_edit_image_fal_nano_banana_pro() {
let client = get_fal_client();
let image_urls = vec![
GHOST_IMAGE_URL.to_string(),
TREX_SKELETON_IMAGE_URL.to_string(),
];
let request = GenerateImageRequest {
prompt: Some("Add the ghost from the first image hovering above the T-Rex skeleton in the second image, make it look spooky but friendly"),
image_inputs: Some(ImageListRef::Urls(&image_urls)),
aspect_ratio: Some(CommonAspectRatio::Auto), // edit mode: preserve source dimensions
resolution: Some(CommonResolution::TwoK),
image_batch_count: Some(1),
..base_fal_image_request()
};
let plan = request.build().unwrap();
let ImageGenerationPlan::FalNanaBananaPro(ref p) = plan else {
panic!("expected FalNanaBananaPro plan");
};
println!("Plan image_urls: {:?}", p.image_urls);
println!("Plan edit_aspect_ratio: {:?}", p.edit_aspect_ratio);
let result = plan.generate_image(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_image request failed");
let payload = response.get_fal_payload().expect("expected Fal payload");
println!("Request ID: {:?}", payload.request_id);
println!("Gateway request ID: {:?}", payload.gateway_request_id);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1 +0,0 @@
pub mod generate_image_fal_nano_banana_pro;
@@ -1,2 +0,0 @@
pub mod artcraft;
pub mod fal;
@@ -1,127 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::plan_generate_image_artcraft_flux_1_dev;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::plan_generate_image_artcraft_flux_1_schnell;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_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_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;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::plan_generate_image_artcraft_nano_banana_pro;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::plan_generate_image_artcraft_seedream_4;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::plan_generate_image_artcraft_seedream_4p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::plan_generate_image_artcraft_seedream_5_lite;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::plan_generate_image_fal_nano_banana_pro;
pub struct GenerateImageRequest<'a> {
/// Which model to use.
pub model: CommonImageModel,
/// Which provider to use.
pub provider: Provider,
/// The prompt for the image generation.
pub prompt: Option<&'a str>,
/// Input images for image editing.
/// If present, we're doing image editing (image-to-image).
/// If absent, we're doing text-to-image.
pub image_inputs: Option<ImageListRef<'a>>,
/// The resolution to use.
pub resolution: Option<CommonResolution>,
/// The aspect ratio to use.
pub aspect_ratio: Option<CommonAspectRatio>,
/// How many images to generate.
pub image_batch_count: Option<u16>,
/// If the request is a mismatch with the (model/provider), how to mitigate it.
pub request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy,
/// Controls behavior when `image_inputs` are supplied to a text-to-image-only model.
/// `None` is equivalent to `GenerateAnyway` — image inputs are silently ignored.
/// Set `AbortGeneration` to return an error instead.
pub generation_mode_mismatch_strategy: Option<GenerationModeMismatchStrategy>,
/// Some providers support idempotency.
/// If not supplied, we'll generate one for the required providers.
pub idempotency_token: Option<&'a str>,
}
impl<'a> GenerateImageRequest<'a> {
/// Read the image generation request, construct a plan, then yield a means to execute it.
pub fn build(&self) -> Result<ImageGenerationPlan<'_>, ArtcraftRouterError> {
match self.provider {
Provider::Artcraft => match self.model {
CommonImageModel::Flux1Dev => {
plan_generate_image_artcraft_flux_1_dev(self).map(ImageGenerationPlan::ArtcraftFlux1Dev)
}
CommonImageModel::Flux1Schnell => {
plan_generate_image_artcraft_flux_1_schnell(self).map(ImageGenerationPlan::ArtcraftFlux1Schnell)
}
CommonImageModel::FluxPro11 => {
plan_generate_image_artcraft_flux_pro_1p1(self).map(ImageGenerationPlan::ArtcraftFluxPro11)
}
CommonImageModel::FluxPro11Ultra => {
plan_generate_image_artcraft_flux_pro_1p1_ultra(self).map(ImageGenerationPlan::ArtcraftFluxPro11Ultra)
}
CommonImageModel::GptImage1p5 => {
plan_generate_image_artcraft_gpt_image_1p5(self).map(ImageGenerationPlan::ArtcraftGptImage1p5)
}
CommonImageModel::NanaBanana => {
plan_generate_image_artcraft_nano_banana(self).map(ImageGenerationPlan::ArtcraftNanaBanana)
}
CommonImageModel::NanaBanana2 => {
plan_generate_image_artcraft_nano_banana_2(self).map(ImageGenerationPlan::ArtcraftNanaBanana2)
}
CommonImageModel::NanaBananaPro => {
plan_generate_image_artcraft_nano_banana_pro(self).map(ImageGenerationPlan::ArtcraftNanaBananaPro)
}
CommonImageModel::Seedream4 => {
plan_generate_image_artcraft_seedream_4(self).map(ImageGenerationPlan::ArtcraftSeedream4)
}
CommonImageModel::Seedream4p5 => {
plan_generate_image_artcraft_seedream_4p5(self).map(ImageGenerationPlan::ArtcraftSeedream4p5)
}
CommonImageModel::Seedream5Lite => {
plan_generate_image_artcraft_seedream_5_lite(self).map(ImageGenerationPlan::ArtcraftSeedream5Lite)
}
},
Provider::Fal => match self.model {
CommonImageModel::NanaBananaPro => {
plan_generate_image_fal_nano_banana_pro(self).map(ImageGenerationPlan::FalNanaBananaPro)
}
CommonImageModel::Flux1Dev
| CommonImageModel::Flux1Schnell
| CommonImageModel::FluxPro11
| CommonImageModel::FluxPro11Ultra
| CommonImageModel::GptImage1p5
| CommonImageModel::NanaBanana
| CommonImageModel::NanaBanana2
| CommonImageModel::Seedream4
| CommonImageModel::Seedream4p5
| CommonImageModel::Seedream5Lite => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "provider",
value: format!("{:?} is only available on the Artcraft provider", self.model),
}))
}
},
}
}
pub fn get_or_generate_idempotency_token(&self) -> String {
self.idempotency_token.map(|t| t.to_string())
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
}
}
@@ -1,34 +0,0 @@
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug)]
pub struct ArtcraftImageResponsePayload {
pub inference_job_token: InferenceJobToken,
}
#[derive(Clone, Debug)]
pub struct FalImageResponsePayload {
pub request_id: Option<String>,
pub gateway_request_id: Option<String>,
}
#[derive(Clone, Debug)]
pub enum GenerateImageResponse {
Artcraft(ArtcraftImageResponsePayload),
Fal(FalImageResponsePayload),
}
impl GenerateImageResponse {
pub fn get_artcraft_payload(&self) -> Option<ArtcraftImageResponsePayload> {
match self {
Self::Artcraft(p) => Some(p.clone()),
_ => None,
}
}
pub fn get_fal_payload(&self) -> Option<FalImageResponsePayload> {
match self {
Self::Fal(p) => Some(p.clone()),
_ => None,
}
}
}
@@ -1,9 +0,0 @@
/// The result of estimating the cost of an image generation plan.
pub struct ImageGenerationCostEstimate {
pub cost_in_credits: Option<u64>,
pub cost_in_usd_cents: Option<u64>,
pub is_free: bool,
pub is_unlimited: bool,
pub is_rate_limited: bool,
pub has_watermark: bool,
}
@@ -1,155 +0,0 @@
use crate::client::router_client::RouterClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_1_dev::estimate_image_cost_artcraft_flux_1_dev;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_1_schnell::estimate_image_cost_artcraft_flux_1_schnell;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1::estimate_image_cost_artcraft_flux_pro_1p1;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_flux_pro_1p1_ultra::estimate_image_cost_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_gpt_image_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;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_nano_banana_pro::estimate_image_cost_artcraft_nano_banana_pro;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4::estimate_image_cost_artcraft_seedream_4;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_4p5::estimate_image_cost_artcraft_seedream_4p5;
use crate::generate::generate_image::cost::artcraft::estimate_image_cost_artcraft_seedream_5_lite::estimate_image_cost_artcraft_seedream_5_lite;
use crate::generate::generate_image::cost::fal::estimate_image_cost_fal_nano_banana_pro::estimate_image_cost_fal_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_dev::execute_artcraft_flux_1_dev;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_1_schnell::execute_artcraft_flux_1_schnell;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1::execute_artcraft_flux_pro_1p1;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_flux_pro_1p1_ultra::execute_artcraft_flux_pro_1p1_ultra;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_gpt_image_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;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_nano_banana_pro::execute_artcraft_nano_banana_pro;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4::execute_artcraft_seedream_4;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_4p5::execute_artcraft_seedream_4p5;
use crate::generate::generate_image::execute::artcraft::generate_image_artcraft_seedream_5_lite::execute_artcraft_seedream_5_lite;
use crate::generate::generate_image::execute::fal::generate_image_fal_nano_banana_pro::execute_fal_nano_banana_pro;
use crate::generate::generate_image::generate_image_response::GenerateImageResponse;
use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_dev::PlanArtcraftFlux1Dev;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_1_schnell::PlanArtcraftFlux1Schnell;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1::PlanArtcraftFluxPro11;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_flux_pro_1p1_ultra::PlanArtcraftFluxPro11Ultra;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_gpt_image_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;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_nano_banana_pro::PlanArtcraftNanaBananaPro;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4::PlanArtcraftSeedream4;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_4p5::PlanArtcraftSeedream4p5;
use crate::generate::generate_image::plan::artcraft::plan_generate_image_artcraft_seedream_5_lite::PlanArtcraftSeedream5Lite;
use crate::generate::generate_image::plan::fal::plan_generate_image_fal_nano_banana_pro::PlanFalNanaBananaPro;
#[derive(Debug)]
pub enum ImageGenerationPlan<'a> {
ArtcraftFlux1Dev(PlanArtcraftFlux1Dev<'a>),
ArtcraftFlux1Schnell(PlanArtcraftFlux1Schnell<'a>),
ArtcraftFluxPro11(PlanArtcraftFluxPro11<'a>),
ArtcraftFluxPro11Ultra(PlanArtcraftFluxPro11Ultra<'a>),
ArtcraftGptImage1p5(PlanArtcraftGptImage1p5<'a>),
ArtcraftNanaBanana(PlanArtcraftNanaBanana<'a>),
ArtcraftNanaBanana2(PlanArtcraftNanaBanana2<'a>),
ArtcraftNanaBananaPro(PlanArtcraftNanaBananaPro<'a>),
ArtcraftSeedream4(PlanArtcraftSeedream4<'a>),
ArtcraftSeedream4p5(PlanArtcraftSeedream4p5<'a>),
ArtcraftSeedream5Lite(PlanArtcraftSeedream5Lite<'a>),
FalNanaBananaPro(PlanFalNanaBananaPro<'a>),
}
impl<'a> ImageGenerationPlan<'a> {
pub async fn generate_image(
&self,
client: &RouterClient,
) -> Result<GenerateImageResponse, ArtcraftRouterError> {
match self {
ImageGenerationPlan::ArtcraftFlux1Dev(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_1_dev(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFlux1Schnell(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_1_schnell(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFluxPro11(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_pro_1p1(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_flux_pro_1p1_ultra(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_gpt_image_1p5(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftNanaBanana(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftNanaBanana2(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana_2(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_nano_banana_pro(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream4(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_4(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream4p5(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_4p5(plan, artcraft_client).await
}
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
let artcraft_client = client.get_artcraft_client_ref()?;
execute_artcraft_seedream_5_lite(plan, artcraft_client).await
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
let fal_client = client.get_fal_client_ref()?;
execute_fal_nano_banana_pro(plan, fal_client).await
}
}
}
pub fn estimate_costs(&self) -> ImageGenerationCostEstimate {
match self {
ImageGenerationPlan::ArtcraftFlux1Dev(plan) => {
estimate_image_cost_artcraft_flux_1_dev(plan)
}
ImageGenerationPlan::ArtcraftFlux1Schnell(plan) => {
estimate_image_cost_artcraft_flux_1_schnell(plan)
}
ImageGenerationPlan::ArtcraftFluxPro11(plan) => {
estimate_image_cost_artcraft_flux_pro_1p1(plan)
}
ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) => {
estimate_image_cost_artcraft_flux_pro_1p1_ultra(plan)
}
ImageGenerationPlan::ArtcraftGptImage1p5(plan) => {
estimate_image_cost_artcraft_gpt_image_1p5(plan)
}
ImageGenerationPlan::ArtcraftNanaBanana(plan) => {
estimate_image_cost_artcraft_nano_banana(plan)
}
ImageGenerationPlan::ArtcraftNanaBanana2(plan) => {
estimate_image_cost_artcraft_nano_banana_2(plan)
}
ImageGenerationPlan::ArtcraftNanaBananaPro(plan) => {
estimate_image_cost_artcraft_nano_banana_pro(plan)
}
ImageGenerationPlan::ArtcraftSeedream4(plan) => {
estimate_image_cost_artcraft_seedream_4(plan)
}
ImageGenerationPlan::ArtcraftSeedream4p5(plan) => {
estimate_image_cost_artcraft_seedream_4p5(plan)
}
ImageGenerationPlan::ArtcraftSeedream5Lite(plan) => {
estimate_image_cost_artcraft_seedream_5_lite(plan)
}
ImageGenerationPlan::FalNanaBananaPro(plan) => {
estimate_image_cost_fal_nano_banana_pro(plan)
}
}
}
}
@@ -1,7 +0,0 @@
pub mod cost;
pub mod execute;
pub mod generate_image_request;
pub mod generate_image_response;
pub mod image_generation_cost_estimate;
pub mod image_generation_plan;
pub mod plan;
@@ -1,11 +0,0 @@
pub mod plan_generate_image_artcraft_flux_1_dev;
pub mod plan_generate_image_artcraft_flux_1_schnell;
pub mod plan_generate_image_artcraft_flux_pro_1p1;
pub mod plan_generate_image_artcraft_flux_pro_1p1_ultra;
pub mod plan_generate_image_artcraft_gpt_image_1p5;
pub mod plan_generate_image_artcraft_nano_banana;
pub mod plan_generate_image_artcraft_nano_banana_2;
pub mod plan_generate_image_artcraft_nano_banana_pro;
pub mod plan_generate_image_artcraft_seedream_4;
pub mod plan_generate_image_artcraft_seedream_4p5;
pub mod plan_generate_image_artcraft_seedream_5_lite;
@@ -1,323 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio, GenerateFlux1DevTextToImageNumImages,
};
#[derive(Debug, Clone)]
pub struct PlanArtcraftFlux1Dev<'a> {
pub prompt: Option<&'a str>,
pub aspect_ratio: Option<GenerateFlux1DevTextToImageAspectRatio>,
pub num_images: GenerateFlux1DevTextToImageNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_1_dev<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFlux1Dev<'a>, ArtcraftRouterError> {
// Flux 1 Dev is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
if let Some(GenerationModeMismatchStrategy::AbortGeneration) = request.generation_mode_mismatch_strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux 1 Dev is a text-to-image model and does not support image editing".to_string(),
}));
}
// None or GenerateAnyway: silently ignore image inputs
}
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFlux1Dev {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
// Flux 1 Dev supported aspect ratios:
// Square, SquareHd, LandscapeFourByThree, LandscapeSixteenByNine,
// PortraitThreeByFour, PortraitNineBySixteen
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GenerateFlux1DevTextToImageAspectRatio>, ArtcraftRouterError> {
use GenerateFlux1DevTextToImageAspectRatio as FlAr;
match aspect_ratio {
None => Ok(None),
// Auto: not valid for text-to-image; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Some(FlAr::Square)),
// Direct mappings
Some(CommonAspectRatio::Square) => Ok(Some(FlAr::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(FlAr::SquareHd)),
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideThreeByTwo) => {
Ok(Some(FlAr::LandscapeFourByThree))
}
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideTwentyOneByNine) => {
Ok(Some(FlAr::LandscapeSixteenByNine))
}
Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(FlAr::PortraitThreeByFour)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineByTwentyOne) => {
Ok(Some(FlAr::PortraitNineBySixteen))
}
// Mismatch — TallFourByFive maps to nearest (LandscapeFourByThree per existing desktop behavior)
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> GenerateFlux1DevTextToImageAspectRatio {
use GenerateFlux1DevTextToImageAspectRatio as FlAr;
match aspect_ratio {
// TallFourByFive (0.8) — nearest is LandscapeFourByThree per existing desktop behavior
CommonAspectRatio::TallFourByFive => FlAr::LandscapeFourByThree,
_ => FlAr::Square,
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GenerateFlux1DevTextToImageNumImages, ArtcraftRouterError> {
use GenerateFlux1DevTextToImageNumImages as FlN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FlN::One),
2 => Ok(FlN::Two),
3 => Ok(FlN::Three),
4 => Ok(FlN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FlN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_1_dev_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_dev_text_to_image::{
GenerateFlux1DevTextToImageAspectRatio as FlAr,
GenerateFlux1DevTextToImageNumImages as FlN,
};
// ── Generation mode mismatch ─────────────────────────────────────────────
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_1_dev_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_1_dev_image_request()
};
assert!(request.build().is_ok());
}
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: None,
..base_flux_1_dev_image_request()
};
assert!(request.build().is_ok());
}
// ── Aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_auto_falls_back_to_square() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, FlAr::Square),
(CommonAspectRatio::SquareHd, FlAr::SquareHd),
(CommonAspectRatio::WideFourByThree, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideThreeByTwo, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideSixteenByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::Wide, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::TallThreeByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallTwoByThree, FlAr::PortraitThreeByFour),
(CommonAspectRatio::WideFiveByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallNineBySixteen, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::Tall, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_dev_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
matches!(plan.aspect_ratio, Some(FlAr::LandscapeFourByThree)),
"expected LandscapeFourByThree nearest match with {:?}", strategy,
);
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_1_dev_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_dev_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_dev_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Dev(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Dev") };
assert!(matches!(plan.num_images, FlN::Four));
}
}
}
@@ -1,319 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio, GenerateFlux1SchnellTextToImageNumImages,
};
#[derive(Debug, Clone)]
pub struct PlanArtcraftFlux1Schnell<'a> {
pub prompt: Option<&'a str>,
pub aspect_ratio: Option<GenerateFlux1SchnellTextToImageAspectRatio>,
pub num_images: GenerateFlux1SchnellTextToImageNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_1_schnell<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFlux1Schnell<'a>, ArtcraftRouterError> {
// Flux 1 Schnell is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
if let Some(GenerationModeMismatchStrategy::AbortGeneration) = request.generation_mode_mismatch_strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux 1 Schnell is a text-to-image model and does not support image editing".to_string(),
}));
}
// None or GenerateAnyway: silently ignore image inputs
}
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFlux1Schnell {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
// Flux 1 Schnell supported aspect ratios (identical to Flux 1 Dev):
// Square, SquareHd, LandscapeFourByThree, LandscapeSixteenByNine,
// PortraitThreeByFour, PortraitNineBySixteen
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GenerateFlux1SchnellTextToImageAspectRatio>, ArtcraftRouterError> {
use GenerateFlux1SchnellTextToImageAspectRatio as FlAr;
match aspect_ratio {
None => Ok(None),
// Auto: not valid for text-to-image; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Some(FlAr::Square)),
// Direct mappings
Some(CommonAspectRatio::Square) => Ok(Some(FlAr::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(FlAr::SquareHd)),
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideThreeByTwo) => {
Ok(Some(FlAr::LandscapeFourByThree))
}
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideTwentyOneByNine) => {
Ok(Some(FlAr::LandscapeSixteenByNine))
}
Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(FlAr::PortraitThreeByFour)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineByTwentyOne) => {
Ok(Some(FlAr::PortraitNineBySixteen))
}
// Mismatch — TallFourByFive maps to nearest (LandscapeFourByThree per existing desktop behavior)
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> GenerateFlux1SchnellTextToImageAspectRatio {
use GenerateFlux1SchnellTextToImageAspectRatio as FlAr;
match aspect_ratio {
// TallFourByFive (0.8) — nearest is LandscapeFourByThree per existing desktop behavior
CommonAspectRatio::TallFourByFive => FlAr::LandscapeFourByThree,
_ => FlAr::Square,
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GenerateFlux1SchnellTextToImageNumImages, ArtcraftRouterError> {
use GenerateFlux1SchnellTextToImageNumImages as FlN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FlN::One),
2 => Ok(FlN::Two),
3 => Ok(FlN::Three),
4 => Ok(FlN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FlN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_1_schnell_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_1_schnell_text_to_image::{
GenerateFlux1SchnellTextToImageAspectRatio as FlAr,
GenerateFlux1SchnellTextToImageNumImages as FlN,
};
// ── Generation mode mismatch ─────────────────────────────────────────────
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_1_schnell_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_1_schnell_image_request()
};
assert!(request.build().is_ok());
}
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: None,
..base_flux_1_schnell_image_request()
};
assert!(request.build().is_ok());
}
// ── Aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_auto_falls_back_to_square() {
for ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, FlAr::Square),
(CommonAspectRatio::SquareHd, FlAr::SquareHd),
(CommonAspectRatio::WideFourByThree, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideThreeByTwo, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideSixteenByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::Wide, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::TallThreeByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallTwoByThree, FlAr::PortraitThreeByFour),
(CommonAspectRatio::WideFiveByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallNineBySixteen, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::Tall, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_schnell_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::LandscapeFourByThree)));
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_1_schnell_image_request() };
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
#[test]
fn num_images_out_of_range_error_out() {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_1_schnell_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_1_schnell_image_request()
};
let ImageGenerationPlan::ArtcraftFlux1Schnell(plan) = request.build().unwrap() else { panic!("expected ArtcraftFlux1Schnell") };
assert!(matches!(plan.num_images, FlN::Four));
}
}
}
@@ -1,323 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
GenerateFluxPro11TextToImageAspectRatio, GenerateFluxPro11TextToImageNumImages,
};
#[derive(Debug, Clone)]
pub struct PlanArtcraftFluxPro11<'a> {
pub prompt: Option<&'a str>,
pub aspect_ratio: Option<GenerateFluxPro11TextToImageAspectRatio>,
pub num_images: GenerateFluxPro11TextToImageNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_pro_1p1<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFluxPro11<'a>, ArtcraftRouterError> {
// Flux Pro 1.1 is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
if let Some(GenerationModeMismatchStrategy::AbortGeneration) = request.generation_mode_mismatch_strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux Pro 1.1 is a text-to-image model and does not support image editing".to_string(),
}));
}
// None or GenerateAnyway: silently ignore image inputs
}
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFluxPro11 {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
// Flux Pro 1.1 supported aspect ratios (no SquareHd):
// Square, LandscapeFourByThree, LandscapeSixteenByNine,
// PortraitThreeByFour, PortraitNineBySixteen
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GenerateFluxPro11TextToImageAspectRatio>, ArtcraftRouterError> {
use GenerateFluxPro11TextToImageAspectRatio as FlAr;
match aspect_ratio {
None => Ok(None),
// Auto and SquareHd: no direct equivalent; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k)
| Some(CommonAspectRatio::SquareHd) => Ok(Some(FlAr::Square)),
// Direct mappings
Some(CommonAspectRatio::Square) => Ok(Some(FlAr::Square)),
Some(CommonAspectRatio::WideFourByThree) | Some(CommonAspectRatio::WideThreeByTwo) => {
Ok(Some(FlAr::LandscapeFourByThree))
}
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideTwentyOneByNine) => {
Ok(Some(FlAr::LandscapeSixteenByNine))
}
Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(FlAr::PortraitThreeByFour)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineByTwentyOne) => {
Ok(Some(FlAr::PortraitNineBySixteen))
}
// Mismatch — TallFourByFive maps to nearest (LandscapeFourByThree per existing desktop behavior)
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> GenerateFluxPro11TextToImageAspectRatio {
use GenerateFluxPro11TextToImageAspectRatio as FlAr;
match aspect_ratio {
// TallFourByFive (0.8) — nearest is LandscapeFourByThree per existing desktop behavior
CommonAspectRatio::TallFourByFive => FlAr::LandscapeFourByThree,
_ => FlAr::Square,
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GenerateFluxPro11TextToImageNumImages, ArtcraftRouterError> {
use GenerateFluxPro11TextToImageNumImages as FlN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FlN::One),
2 => Ok(FlN::Two),
3 => Ok(FlN::Three),
4 => Ok(FlN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FlN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_pro_1p1_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_text_to_image::{
GenerateFluxPro11TextToImageAspectRatio as FlAr,
GenerateFluxPro11TextToImageNumImages as FlN,
};
// ── Generation mode mismatch ─────────────────────────────────────────────
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_pro_1p1_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_pro_1p1_image_request()
};
assert!(request.build().is_ok());
}
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: None,
..base_flux_pro_1p1_image_request()
};
assert!(request.build().is_ok());
}
// ── Aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_auto_and_square_hd_fall_back_to_square() {
for ar in [
CommonAspectRatio::Auto,
CommonAspectRatio::Auto2k,
CommonAspectRatio::Auto4k,
CommonAspectRatio::SquareHd,
] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, FlAr::Square),
(CommonAspectRatio::WideFourByThree, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideThreeByTwo, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideSixteenByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::Wide, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::TallThreeByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallTwoByThree, FlAr::PortraitThreeByFour),
(CommonAspectRatio::WideFiveByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallNineBySixteen, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::Tall, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_pro_1p1_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_image_request()
};
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::LandscapeFourByThree)));
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_pro_1p1_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
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_flux_pro_1p1_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_image_request()
};
let ImageGenerationPlan::ArtcraftFluxPro11(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11") };
assert!(matches!(plan.num_images, FlN::Four));
}
}
}
@@ -1,326 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
GenerateFluxPro11UltraTextToImageAspectRatio, GenerateFluxPro11UltraTextToImageNumImages,
};
#[derive(Debug, Clone)]
pub struct PlanArtcraftFluxPro11Ultra<'a> {
pub prompt: Option<&'a str>,
pub aspect_ratio: Option<GenerateFluxPro11UltraTextToImageAspectRatio>,
pub num_images: GenerateFluxPro11UltraTextToImageNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_flux_pro_1p1_ultra<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftFluxPro11Ultra<'a>, ArtcraftRouterError> {
// Flux Pro 1.1 Ultra is text-to-image only. Abort if image inputs are provided and
// the caller has opted into strict mode.
if request.image_inputs.is_some() {
if let Some(GenerationModeMismatchStrategy::AbortGeneration) = request.generation_mode_mismatch_strategy {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "Flux Pro 1.1 Ultra is a text-to-image model and does not support image editing".to_string(),
}));
}
// None or GenerateAnyway: silently ignore image inputs
}
let strategy = request.request_mismatch_mitigation_strategy;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftFluxPro11Ultra {
prompt: request.prompt,
aspect_ratio,
num_images,
idempotency_token: request.get_or_generate_idempotency_token(),
})
}
// Flux Pro 1.1 Ultra supported aspect ratios:
// Square, LandscapeThreeByTwo, LandscapeFourByThree, LandscapeSixteenByNine,
// LandscapeTwentyOneByNine, PortraitTwoByThree, PortraitThreeByFour,
// PortraitNineBySixteen, PortraitNineByTwentyOne
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GenerateFluxPro11UltraTextToImageAspectRatio>, ArtcraftRouterError> {
use GenerateFluxPro11UltraTextToImageAspectRatio as FlAr;
match aspect_ratio {
None => Ok(None),
// Auto and SquareHd: no direct equivalent; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k)
| Some(CommonAspectRatio::SquareHd) => Ok(Some(FlAr::Square)),
// Direct mappings
Some(CommonAspectRatio::Square) => Ok(Some(FlAr::Square)),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(FlAr::LandscapeThreeByTwo)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(FlAr::LandscapeFourByThree)),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
Ok(Some(FlAr::LandscapeSixteenByNine))
}
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(FlAr::LandscapeTwentyOneByNine)),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(FlAr::PortraitTwoByThree)),
Some(CommonAspectRatio::TallThreeByFour) | Some(CommonAspectRatio::WideFiveByFour) => {
Ok(Some(FlAr::PortraitThreeByFour))
}
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
Ok(Some(FlAr::PortraitNineBySixteen))
}
Some(CommonAspectRatio::TallNineByTwentyOne) => Ok(Some(FlAr::PortraitNineByTwentyOne)),
// Mismatch — TallFourByFive maps to nearest (LandscapeFourByThree per existing desktop behavior)
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> GenerateFluxPro11UltraTextToImageAspectRatio {
use GenerateFluxPro11UltraTextToImageAspectRatio as FlAr;
match aspect_ratio {
// TallFourByFive (0.8) — nearest is LandscapeFourByThree per existing desktop behavior
CommonAspectRatio::TallFourByFive => FlAr::LandscapeFourByThree,
_ => FlAr::Square,
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GenerateFluxPro11UltraTextToImageNumImages, ArtcraftRouterError> {
use GenerateFluxPro11UltraTextToImageNumImages as FlN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FlN::One),
2 => Ok(FlN::Two),
3 => Ok(FlN::Three),
4 => Ok(FlN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FlN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_flux_pro_1p1_ultra_image_request;
use artcraft_api_defs::generate::image::text::generate_flux_pro_11_ultra_text_to_image::{
GenerateFluxPro11UltraTextToImageAspectRatio as FlAr,
GenerateFluxPro11UltraTextToImageNumImages as FlN,
};
// ── Generation mode mismatch ─────────────────────────────────────────────
#[test]
fn image_inputs_with_abort_returns_error() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::AbortGeneration),
..base_flux_pro_1p1_ultra_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field: "image_inputs", .. }))
));
}
#[test]
fn image_inputs_with_generate_anyway_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
..base_flux_pro_1p1_ultra_image_request()
};
assert!(request.build().is_ok());
}
#[test]
fn image_inputs_with_none_strategy_succeeds() {
let tokens = vec![];
let request = GenerateImageRequest {
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
generation_mode_mismatch_strategy: None,
..base_flux_pro_1p1_ultra_image_request()
};
assert!(request.build().is_ok());
}
// ── Aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_auto_and_square_hd_fall_back_to_square() {
for ar in [
CommonAspectRatio::Auto,
CommonAspectRatio::Auto2k,
CommonAspectRatio::Auto4k,
CommonAspectRatio::SquareHd,
] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, FlAr::Square),
(CommonAspectRatio::WideThreeByTwo, FlAr::LandscapeThreeByTwo),
(CommonAspectRatio::WideFourByThree, FlAr::LandscapeFourByThree),
(CommonAspectRatio::WideSixteenByNine, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::Wide, FlAr::LandscapeSixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, FlAr::LandscapeTwentyOneByNine),
(CommonAspectRatio::TallTwoByThree, FlAr::PortraitTwoByThree),
(CommonAspectRatio::TallThreeByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::WideFiveByFour, FlAr::PortraitThreeByFour),
(CommonAspectRatio::TallNineBySixteen, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::Tall, FlAr::PortraitNineBySixteen),
(CommonAspectRatio::TallNineByTwentyOne, FlAr::PortraitNineByTwentyOne),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_flux_pro_1p1_ultra_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_ultra_image_request()
};
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(matches!(plan.aspect_ratio, Some(FlAr::LandscapeFourByThree)));
}
}
// ── Num images ───────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_ultra_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))
));
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, FlN::One), (2, FlN::Two), (3, FlN::Three), (4, FlN::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_flux_pro_1p1_ultra_image_request() };
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
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_flux_pro_1p1_ultra_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_flux_pro_1p1_ultra_image_request()
};
let ImageGenerationPlan::ArtcraftFluxPro11Ultra(plan) = request.build().unwrap() else { panic!("expected ArtcraftFluxPro11Ultra") };
assert!(matches!(plan.num_images, FlN::Four));
}
}
}
@@ -1,310 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages, GptImage1p5MultiFunctionImageGenQuality,
GptImage1p5MultiFunctionImageGenSize,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftGptImage1p5<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub image_size: Option<GptImage1p5MultiFunctionImageGenSize>,
/// Quality defaults to Medium when not specified in the request.
pub quality: GptImage1p5MultiFunctionImageGenQuality,
pub num_images: GptImage1p5MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_gpt_image_1p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftGptImage1p5<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let image_inputs = 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(PlanArtcraftGptImage1p5 {
prompt: request.prompt,
image_inputs,
image_size,
quality: GptImage1p5MultiFunctionImageGenQuality::Medium,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// GPT Image 1.5 image sizes: Square (1024x1024), Wide (1536x1024), Tall (1024x1536).
// All 17 CommonAspectRatio variants map to one of these three sizes or None.
// Auto aspect ratios yield None (let the API choose its default).
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<GptImage1p5MultiFunctionImageGenSize>, ArtcraftRouterError> {
use GptImage1p5MultiFunctionImageGenSize as GS;
match aspect_ratio {
// No preference or auto — let the model use its default
None
| Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(None),
// Square
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(GS::Square)),
// Wide
Some(CommonAspectRatio::WideThreeByTwo)
| Some(CommonAspectRatio::WideFourByThree)
| Some(CommonAspectRatio::WideFiveByFour)
| Some(CommonAspectRatio::WideSixteenByNine)
| Some(CommonAspectRatio::WideTwentyOneByNine)
| Some(CommonAspectRatio::Wide) => Ok(Some(GS::Wide)),
// Tall
Some(CommonAspectRatio::TallTwoByThree)
| Some(CommonAspectRatio::TallThreeByFour)
| Some(CommonAspectRatio::TallFourByFive)
| Some(CommonAspectRatio::TallNineBySixteen)
| Some(CommonAspectRatio::TallNineByTwentyOne)
| Some(CommonAspectRatio::Tall) => Ok(Some(GS::Tall)),
}
}
// GPT Image 1.5 supports 1, 2, 3, and 4 images.
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<GptImage1p5MultiFunctionImageGenNumImages, ArtcraftRouterError> {
use GptImage1p5MultiFunctionImageGenNumImages as GN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(GN::One),
2 => Ok(GN::Two),
3 => Ok(GN::Three),
4 => Ok(GN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(GN::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(GN::Four),
},
}
}
#[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::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_gpt_image_1p5_image_request;
use artcraft_api_defs::generate::image::multi_function::gpt_image_1p5_multi_function_image_gen::{
GptImage1p5MultiFunctionImageGenNumImages as GN,
GptImage1p5MultiFunctionImageGenQuality,
GptImage1p5MultiFunctionImageGenSize as GS,
};
// ── Image size (aspect ratio mapping) ────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
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_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
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_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.image_size, Some(GS::Square)), "expected Square for {:?}", ar);
}
}
#[test]
fn image_size_wide_variants() {
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_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.image_size, Some(GS::Wide)), "expected Wide for {:?}", ar);
}
}
#[test]
fn image_size_tall_variants() {
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_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(matches!(plan.image_size, Some(GS::Tall)), "expected Tall for {:?}", ar);
}
}
// ── Quality ──────────────────────────────────────────────────────────────
#[test]
fn quality_defaults_to_medium() {
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));
}
// ── 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_1p5_image_request()
};
let result = request.build();
assert!(
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, GN::One),
(2, GN::Two),
(3, GN::Three),
(4, GN::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
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_1p5_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(5),
request_mismatch_mitigation_strategy: strategy,
..base_gpt_image_1p5_image_request()
};
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(
matches!(plan.num_images, GN::Four),
"expected Four for count 5 with {:?}", strategy,
);
}
}
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
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))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_gpt_image_1p5_image_request() };
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
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_1p5_image_request()
};
let ImageGenerationPlan::ArtcraftGptImage1p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftGptImage1p5") };
assert!(plan.image_inputs.is_some());
}
}
@@ -1,389 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
NanoBananaMultiFunctionImageGenAspectRatio, NanoBananaMultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftNanaBanana<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub aspect_ratio: Option<NanoBananaMultiFunctionImageGenAspectRatio>,
pub num_images: NanoBananaMultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_nano_banana<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBanana<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftNanaBanana {
prompt: request.prompt,
image_inputs,
aspect_ratio,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Nano Banana supported aspect ratios:
// Text-to-image: OneByOne, FiveByFour, FourByThree, ThreeByTwo, SixteenByNine, TwentyOneByNine,
// FourByFive, ThreeByFour, TwoByThree, NineBySixteen
// Image editing: Auto (uses source image AR), plus all text-to-image ratios
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanoBananaMultiFunctionImageGenAspectRatio>, ArtcraftRouterError> {
use NanoBananaMultiFunctionImageGenAspectRatio as NbAr;
match aspect_ratio {
// No preference — let the model use its default
None => Ok(None),
// Auto: only valid in edit mode (use source image dimensions)
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => {
if is_edit_mode {
Ok(Some(NbAr::Auto))
} else {
// Auto not valid for text-to-image; fall back to square default
Ok(Some(NbAr::OneByOne))
}
}
// Direct mappings
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(NbAr::OneByOne)),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(NbAr::FiveByFour)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(NbAr::FourByThree)),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(NbAr::ThreeByTwo)),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(NbAr::SixteenByNine)),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(NbAr::TwentyOneByNine)),
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(NbAr::FourByFive)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(NbAr::ThreeByFour)),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(NbAr::TwoByThree)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(NbAr::NineBySixteen)),
// Mismatch — TallNineByTwentyOne has no direct equivalent
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
/// Pick the nearest supported aspect ratio for unsupported inputs.
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> NanoBananaMultiFunctionImageGenAspectRatio {
use NanoBananaMultiFunctionImageGenAspectRatio as NbAr;
match aspect_ratio {
// TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625)
CommonAspectRatio::TallNineByTwentyOne => NbAr::NineBySixteen,
_ => NbAr::OneByOne,
}
}
// Nano Banana supports 1, 2, 3, and 4 images.
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<NanoBananaMultiFunctionImageGenNumImages, ArtcraftRouterError> {
use NanoBananaMultiFunctionImageGenNumImages as NbN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(NbN::One),
2 => Ok(NbN::Two),
3 => Ok(NbN::Three),
4 => Ok(NbN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(NbN::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(NbN::Four),
},
}
}
#[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::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_nano_banana_image_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_multi_function_image_gen::{
NanoBananaMultiFunctionImageGenAspectRatio as NbAr,
NanoBananaMultiFunctionImageGenNumImages as NbN,
};
// ── Aspect ratio ────────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_direct_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(matches!(plan.aspect_ratio, Some(NbAr::OneByOne)), "expected OneByOne for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_wide() {
let cases = [
(CommonAspectRatio::WideFiveByFour, NbAr::FiveByFour),
(CommonAspectRatio::WideFourByThree, NbAr::FourByThree),
(CommonAspectRatio::WideThreeByTwo, NbAr::ThreeByTwo),
(CommonAspectRatio::WideSixteenByNine, NbAr::SixteenByNine),
(CommonAspectRatio::Wide, NbAr::SixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, NbAr::TwentyOneByNine),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_direct_tall() {
let cases = [
(CommonAspectRatio::TallFourByFive, NbAr::FourByFive),
(CommonAspectRatio::TallThreeByFour, NbAr::ThreeByFour),
(CommonAspectRatio::TallTwoByThree, NbAr::TwoByThree),
(CommonAspectRatio::TallNineBySixteen, NbAr::NineBySixteen),
(CommonAspectRatio::Tall, NbAr::NineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_auto_in_edit_mode_yields_auto() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_nano_banana_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(NbAr::Auto)),
"expected Auto in edit mode for {:?}", auto_ar,
);
}
}
#[test]
fn aspect_ratio_auto_in_text_to_image_mode_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_nano_banana_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(NbAr::OneByOne)),
"expected OneByOne fallback in text-to-image for {:?}", auto_ar,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
// TallNineByTwentyOne has no direct equivalent
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_nano_banana_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
// TallNineByTwentyOne (0.43) is nearest to NineBySixteen (0.5625)
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: strategy,
..base_nano_banana_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.aspect_ratio, Some(NbAr::NineBySixteen)),
"expected NineBySixteen nearest match with {:?}", strategy,
);
}
}
// ── Num images ──────────────────────────────────────────────────────────────
#[test]
fn num_images_zero_is_always_error() {
for strategy in [
RequestMismatchMitigationStrategy::ErrorOut,
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
image_batch_count: Some(0),
request_mismatch_mitigation_strategy: strategy,
..base_nano_banana_image_request()
};
let result = request.build();
assert!(
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, NbN::One),
(2, NbN::Two),
(3, NbN::Three),
(4, NbN::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
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_nano_banana_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(5),
request_mismatch_mitigation_strategy: strategy,
..base_nano_banana_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(
matches!(plan.num_images, NbN::Four),
"expected Four for count 5 with {:?}", strategy,
);
}
}
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
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))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_nano_banana_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
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_nano_banana_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana") };
assert!(plan.image_inputs.is_some());
}
}
@@ -1,289 +0,0 @@
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 artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
NanaBanana2MultiFunctionImageGenAspectRatio, NanaBanana2MultiFunctionImageGenImageResolution,
NanaBanana2MultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftNanaBanana2<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub aspect_ratio: Option<NanaBanana2MultiFunctionImageGenAspectRatio>,
pub resolution: Option<NanaBanana2MultiFunctionImageGenImageResolution>,
pub num_images: NanaBanana2MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_nano_banana_2<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBanana2<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
let resolution = plan_resolution(request.resolution, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftNanaBanana2 {
prompt: request.prompt,
image_inputs,
aspect_ratio,
resolution,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Nano Banana 2 supported aspect ratios:
// Text-to-image: OneByOne, FiveByFour, FourByThree, ThreeByTwo, SixteenByNine, TwentyOneByNine,
// FourByFive, ThreeByFour, TwoByThree, NineBySixteen
// Image editing: Auto (uses source image AR), plus all text-to-image ratios
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanaBanana2MultiFunctionImageGenAspectRatio>, ArtcraftRouterError> {
use NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar;
match aspect_ratio {
// No preference — let the model use its default
None => Ok(None),
// Auto: only valid in edit mode (use source image dimensions)
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => {
if is_edit_mode {
Ok(Some(Nb2Ar::Auto))
} else {
// Auto not valid for text-to-image; fall back to square default
Ok(Some(Nb2Ar::OneByOne))
}
}
// Direct mappings
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(Nb2Ar::OneByOne)),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(Nb2Ar::FiveByFour)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(Nb2Ar::FourByThree)),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(Nb2Ar::ThreeByTwo)),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(Nb2Ar::SixteenByNine)),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(Nb2Ar::TwentyOneByNine)),
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(Nb2Ar::FourByFive)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(Nb2Ar::ThreeByFour)),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(Nb2Ar::TwoByThree)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(Nb2Ar::NineBySixteen)),
// Mismatch — TallNineByTwentyOne has no direct equivalent
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
/// Pick the nearest supported aspect ratio for unsupported inputs.
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> NanaBanana2MultiFunctionImageGenAspectRatio {
use NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar;
match aspect_ratio {
// TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625)
CommonAspectRatio::TallNineByTwentyOne => Nb2Ar::NineBySixteen,
_ => Nb2Ar::OneByOne,
}
}
fn plan_resolution(
resolution: Option<CommonResolution>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanaBanana2MultiFunctionImageGenImageResolution>, ArtcraftRouterError> {
use CommonResolution as R;
use NanaBanana2MultiFunctionImageGenImageResolution as Nb2Res;
match resolution {
None => Ok(None),
Some(R::OneK) => Ok(Some(Nb2Res::OneK)),
Some(R::TwoK) => Ok(Some(Nb2Res::TwoK)),
Some(R::FourK) => Ok(Some(Nb2Res::FourK)),
Some(R::ThreeK) => {
// No 3K option; default to 2K
Ok(Some(Nb2Res::TwoK))
}
}
}
// Nano Banana 2 supports 1, 2, 3, and 4 images.
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<NanaBanana2MultiFunctionImageGenNumImages, ArtcraftRouterError> {
use NanaBanana2MultiFunctionImageGenNumImages as Nb2N;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(Nb2N::One),
2 => Ok(Nb2N::Two),
3 => Ok(Nb2N::Three),
4 => Ok(Nb2N::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Nb2N::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Nb2N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_nano_banana_2_image_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_2_multi_function_image_gen::{
NanaBanana2MultiFunctionImageGenAspectRatio as Nb2Ar,
NanaBanana2MultiFunctionImageGenImageResolution as Nb2Res,
NanaBanana2MultiFunctionImageGenNumImages as Nb2N,
};
// ── Aspect ratio ────────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_direct_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(matches!(plan.aspect_ratio, Some(Nb2Ar::OneByOne)), "expected OneByOne for {:?}", ar);
}
}
#[test]
fn aspect_ratio_auto_in_edit_mode_yields_auto() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_nano_banana_2_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(
matches!(plan.aspect_ratio, Some(Nb2Ar::Auto)),
"expected Auto in edit mode for {:?}", auto_ar,
);
}
}
#[test]
fn aspect_ratio_auto_in_text_to_image_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_nano_banana_2_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(
matches!(plan.aspect_ratio, Some(Nb2Ar::OneByOne)),
"expected OneByOne fallback in text-to-image for {:?}", auto_ar,
);
}
}
// ── Resolution ──────────────────────────────────────────────────────────────
#[test]
fn resolution_none_is_none() {
let request = GenerateImageRequest { resolution: None, ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(plan.resolution.is_none());
}
#[test]
fn resolution_direct_mappings() {
let cases = [
(CommonResolution::OneK, Nb2Res::OneK),
(CommonResolution::TwoK, Nb2Res::TwoK),
(CommonResolution::FourK, Nb2Res::FourK),
];
for (common, expected) in cases {
let request = GenerateImageRequest { resolution: Some(common), ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn resolution_three_k_falls_back_to_two_k() {
let request = GenerateImageRequest {
resolution: Some(CommonResolution::ThreeK),
..base_nano_banana_2_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(matches!(plan.resolution, Some(Nb2Res::TwoK)));
}
// ── Num images ──────────────────────────────────────────────────────────────
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, Nb2N::One),
(2, Nb2N::Two),
(3, Nb2N::Three),
(4, Nb2N::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_nano_banana_2_image_request() };
let ImageGenerationPlan::ArtcraftNanaBanana2(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBanana2") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
}
@@ -1,451 +0,0 @@
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 artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenAspectRatio, NanoBananaProMultiFunctionImageGenImageResolution,
NanoBananaProMultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftNanaBananaPro<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub aspect_ratio: Option<NanoBananaProMultiFunctionImageGenAspectRatio>,
pub resolution: Option<NanoBananaProMultiFunctionImageGenImageResolution>,
pub num_images: NanoBananaProMultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_nano_banana_pro<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftNanaBananaPro<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_inputs = resolve_image_list_ref(request.image_inputs)?;
let aspect_ratio = plan_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
let resolution = plan_resolution(request.resolution, strategy)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanArtcraftNanaBananaPro {
prompt: request.prompt,
image_inputs,
aspect_ratio,
resolution,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Nano Banana Pro supported aspect ratios:
// Text-to-image: OneByOne, FiveByFour, FourByThree, ThreeByTwo, SixteenByNine, TwentyOneByNine,
// FourByFive, ThreeByFour, TwoByThree, NineBySixteen
// Image editing: Auto (uses source image AR), plus all text-to-image ratios
fn plan_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanoBananaProMultiFunctionImageGenAspectRatio>, ArtcraftRouterError> {
use NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr;
match aspect_ratio {
// No preference — let the model use its default
None => Ok(None),
// Auto: only valid in edit mode (use source image dimensions)
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => {
if is_edit_mode {
Ok(Some(NbpAr::Auto))
} else {
// Auto not valid for text-to-image; fall back to square default
Ok(Some(NbpAr::OneByOne))
}
}
// Direct mappings
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(NbpAr::OneByOne)),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(NbpAr::FiveByFour)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(NbpAr::FourByThree)),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(NbpAr::ThreeByTwo)),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(NbpAr::SixteenByNine)),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(NbpAr::TwentyOneByNine)),
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(NbpAr::FourByFive)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(NbpAr::ThreeByFour)),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(NbpAr::TwoByThree)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(NbpAr::NineBySixteen)),
// Mismatch — TallNineByTwentyOne has no direct equivalent
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_aspect_ratio(unsupported)))
}
},
}
}
/// Pick the nearest supported aspect ratio for unsupported inputs.
fn nearest_aspect_ratio(aspect_ratio: CommonAspectRatio) -> NanoBananaProMultiFunctionImageGenAspectRatio {
use NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr;
match aspect_ratio {
// TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625)
CommonAspectRatio::TallNineByTwentyOne => NbpAr::NineBySixteen,
_ => NbpAr::OneByOne,
}
}
fn plan_resolution(
resolution: Option<CommonResolution>,
_strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<NanoBananaProMultiFunctionImageGenImageResolution>, ArtcraftRouterError> {
use CommonResolution as R;
use NanoBananaProMultiFunctionImageGenImageResolution as NbpRes;
match resolution {
None => Ok(None),
Some(R::OneK) => Ok(Some(NbpRes::OneK)),
Some(R::TwoK) => Ok(Some(NbpRes::TwoK)),
Some(R::FourK) => Ok(Some(NbpRes::FourK)),
Some(R::ThreeK) => {
// No 3K option; default to 2K
Ok(Some(NbpRes::TwoK))
}
}
}
// Nano Banana Pro supports 1, 2, 3, and 4 images.
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<NanoBananaProMultiFunctionImageGenNumImages, ArtcraftRouterError> {
use NanoBananaProMultiFunctionImageGenNumImages as NbpN;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(NbpN::One),
2 => Ok(NbpN::Two),
3 => Ok(NbpN::Three),
4 => Ok(NbpN::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(NbpN::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(NbpN::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_image_request;
use artcraft_api_defs::generate::image::multi_function::nano_banana_pro_multi_function_image_gen::{
NanoBananaProMultiFunctionImageGenAspectRatio as NbpAr,
NanoBananaProMultiFunctionImageGenImageResolution as NbpRes,
NanoBananaProMultiFunctionImageGenNumImages as NbpN,
};
// ── Aspect ratio ────────────────────────────────────────────────────────────
#[test]
fn aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(plan.aspect_ratio.is_none());
}
#[test]
fn aspect_ratio_direct_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(matches!(plan.aspect_ratio, Some(NbpAr::OneByOne)), "expected OneByOne for {:?}", ar);
}
}
#[test]
fn aspect_ratio_direct_wide() {
let cases = [
(CommonAspectRatio::WideFiveByFour, NbpAr::FiveByFour),
(CommonAspectRatio::WideFourByThree, NbpAr::FourByThree),
(CommonAspectRatio::WideThreeByTwo, NbpAr::ThreeByTwo),
(CommonAspectRatio::WideSixteenByNine, NbpAr::SixteenByNine),
(CommonAspectRatio::Wide, NbpAr::SixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, NbpAr::TwentyOneByNine),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_direct_tall() {
let cases = [
(CommonAspectRatio::TallFourByFive, NbpAr::FourByFive),
(CommonAspectRatio::TallThreeByFour, NbpAr::ThreeByFour),
(CommonAspectRatio::TallTwoByThree, NbpAr::TwoByThree),
(CommonAspectRatio::TallNineBySixteen, NbpAr::NineBySixteen),
(CommonAspectRatio::Tall, NbpAr::NineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn aspect_ratio_auto_in_edit_mode_yields_auto() {
// Auto is only valid for edit mode (image_inputs present)
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(NbpAr::Auto)),
"expected Auto in edit mode for {:?}", auto_ar,
);
}
}
#[test]
fn aspect_ratio_auto_in_text_to_image_mode_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(NbpAr::OneByOne)),
"expected OneByOne fallback in text-to-image for {:?}", auto_ar,
);
}
}
#[test]
fn aspect_ratio_unsupported_error_out() {
// TallNineByTwentyOne has no direct equivalent
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_image_request()
};
let result = request.build();
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn aspect_ratio_unsupported_nearest_match() {
// TallNineByTwentyOne (0.43) is nearest to NineBySixteen (0.5625)
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: strategy,
..base_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.aspect_ratio, Some(NbpAr::NineBySixteen)),
"expected NineBySixteen nearest match with {:?}", strategy,
);
}
}
// ── Resolution ──────────────────────────────────────────────────────────────
#[test]
fn resolution_none_is_none() {
let request = GenerateImageRequest { resolution: None, ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(plan.resolution.is_none());
}
#[test]
fn resolution_direct_mappings() {
let cases = [
(CommonResolution::OneK, NbpRes::OneK),
(CommonResolution::TwoK, NbpRes::TwoK),
(CommonResolution::FourK, NbpRes::FourK),
];
for (common, expected) in cases {
let request = GenerateImageRequest { resolution: Some(common), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn resolution_three_k_falls_back_to_two_k() {
let request = GenerateImageRequest {
resolution: Some(CommonResolution::ThreeK),
..base_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(matches!(plan.resolution, Some(NbpRes::TwoK)));
}
// ── 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_image_request()
};
let result = request.build();
assert!(
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, NbpN::One),
(2, NbpN::Two),
(3, NbpN::Three),
(4, NbpN::Four),
];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
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_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(5),
request_mismatch_mitigation_strategy: strategy,
..base_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(
matches!(plan.num_images, NbpN::Four),
"expected Four for count 5 with {:?}", strategy,
);
}
}
// ── Image inputs ─────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
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))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_image_request() };
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
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_image_request()
};
let ImageGenerationPlan::ArtcraftNanaBananaPro(plan) = request.build().unwrap() else { panic!("expected ArtcraftNanaBananaPro") };
assert!(plan.image_inputs.is_some());
}
}
@@ -1,380 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
BytedanceSeedreamV4MultiFunctionImageGenImageSize,
BytedanceSeedreamV4MultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftSeedream4<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub image_size: Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize>,
pub num_images: BytedanceSeedreamV4MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_seedream_4<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream4<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_inputs = 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(PlanArtcraftSeedream4 {
prompt: request.prompt,
image_inputs,
image_size,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Seedream V4 supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
// Auto (edit mode: preserve source dimensions), Auto2k, Auto4k
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<BytedanceSeedreamV4MultiFunctionImageGenImageSize>, ArtcraftRouterError> {
use BytedanceSeedreamV4MultiFunctionImageGenImageSize as S;
match aspect_ratio {
None => Ok(None),
// Auto: valid in edit mode (preserve source dimensions); fall back to Square for text-to-image
Some(CommonAspectRatio::Auto) => {
if is_edit_mode { Ok(Some(S::Auto)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
// Square
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
// Wide — direct mappings
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
// Wide — no direct equivalent, nearest is LandscapeSixteenNine
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),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::LandscapeSixteenNine)),
},
// Tall — direct mappings
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
// Tall — no direct equivalent, nearest is PortraitSixteenNine
Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::PortraitSixteenNine)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<BytedanceSeedreamV4MultiFunctionImageGenNumImages, ArtcraftRouterError> {
use BytedanceSeedreamV4MultiFunctionImageGenNumImages as N;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(N::One),
2 => Ok(N::Two),
3 => Ok(N::Three),
4 => Ok(N::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::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::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_seedream_4_image_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4_multi_function_image_gen::{
BytedanceSeedreamV4MultiFunctionImageGenImageSize as S,
BytedanceSeedreamV4MultiFunctionImageGenNumImages as N,
};
// ── Image size (from aspect ratio) ──────────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
let expected = match ar {
CommonAspectRatio::Square => S::Square,
CommonAspectRatio::SquareHd => S::SquareHd,
_ => unreachable!(),
};
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_wide_direct() {
let cases = [
(CommonAspectRatio::Wide, S::LandscapeSixteenNine),
(CommonAspectRatio::WideSixteenByNine, S::LandscapeSixteenNine),
(CommonAspectRatio::WideFourByThree, S::LandscapeFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_tall_direct() {
let cases = [
(CommonAspectRatio::Tall, S::PortraitSixteenNine),
(CommonAspectRatio::TallNineBySixteen, S::PortraitSixteenNine),
(CommonAspectRatio::TallThreeByFour, S::PortraitFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_auto_in_edit_mode_yields_auto() {
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::Auto)));
}
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn image_size_auto2k_and_auto4k() {
for (ar, expected) in [(CommonAspectRatio::Auto2k, S::Auto2k), (CommonAspectRatio::Auto4k, S::Auto4k)] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_unsupported_error_out() {
for ar in [CommonAspectRatio::WideFiveByFour, CommonAspectRatio::TallFourByFive] {
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
), "expected error for {:?}", ar);
}
}
#[test]
fn image_size_unsupported_nearest_match() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideFiveByFour),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::LandscapeSixteenNine)), "expected LandscapeSixteenNine with {:?}", strategy);
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.image_size, Some(S::PortraitSixteenNine)), "expected PortraitSixteenNine with {:?}", strategy);
}
}
// ── 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_seedream_4_image_request()
};
assert!(
matches!(request.build(), Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
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_seedream_4_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(matches!(plan.num_images, N::Four), "expected Four for count 5 with {:?}", strategy);
}
}
// ── Image inputs ──────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
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))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_seedream_4_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
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_seedream_4_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4") };
assert!(plan.image_inputs.is_some());
}
}
@@ -1,381 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftSeedream4p5<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub image_size: Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>,
pub num_images: BytedanceSeedreamV4p5MultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_seedream_4p5<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream4p5<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_inputs = 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(PlanArtcraftSeedream4p5 {
prompt: request.prompt,
image_inputs,
image_size,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Seedream V4.5 supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
// Auto2k, Auto4k (no bare Auto — unlike V4)
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<BytedanceSeedreamV4p5MultiFunctionImageGenImageSize>, ArtcraftRouterError> {
use BytedanceSeedreamV4p5MultiFunctionImageGenImageSize as S;
match aspect_ratio {
None => Ok(None),
// Auto: V4.5 has no bare Auto; fall back to Auto2k in edit mode, Square for text-to-image
Some(CommonAspectRatio::Auto) => {
if is_edit_mode { Ok(Some(S::Auto2k)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto4k)),
// Square
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
// Wide — direct mappings
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
// Wide — no direct equivalent, nearest is LandscapeSixteenNine
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),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::LandscapeSixteenNine)),
},
// Tall — direct mappings
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
// Tall — no direct equivalent, nearest is PortraitSixteenNine
Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::PortraitSixteenNine)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<BytedanceSeedreamV4p5MultiFunctionImageGenNumImages, ArtcraftRouterError> {
use BytedanceSeedreamV4p5MultiFunctionImageGenNumImages as N;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(N::One),
2 => Ok(N::Two),
3 => Ok(N::Three),
4 => Ok(N::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::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::generate::generate_image::image_generation_plan::ImageGenerationPlan;
use crate::test_helpers::base_seedream_4p5_image_request;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_v4p5_multi_function_image_gen::{
BytedanceSeedreamV4p5MultiFunctionImageGenImageSize as S,
BytedanceSeedreamV4p5MultiFunctionImageGenNumImages as N,
};
// ── Image size (from aspect ratio) ──────────────────────────────────────────
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_square() {
for ar in [CommonAspectRatio::Square, CommonAspectRatio::SquareHd] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
let expected = match ar {
CommonAspectRatio::Square => S::Square,
CommonAspectRatio::SquareHd => S::SquareHd,
_ => unreachable!(),
};
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_wide_direct() {
let cases = [
(CommonAspectRatio::Wide, S::LandscapeSixteenNine),
(CommonAspectRatio::WideSixteenByNine, S::LandscapeSixteenNine),
(CommonAspectRatio::WideFourByThree, S::LandscapeFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_tall_direct() {
let cases = [
(CommonAspectRatio::Tall, S::PortraitSixteenNine),
(CommonAspectRatio::TallNineBySixteen, S::PortraitSixteenNine),
(CommonAspectRatio::TallThreeByFour, S::PortraitFourThree),
];
for (ar, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_auto_in_edit_mode_yields_auto2k() {
// V4.5 has no bare Auto; falls back to Auto2k in edit mode
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::Auto2k)));
}
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn image_size_auto2k_and_auto4k() {
for (ar, expected) in [(CommonAspectRatio::Auto2k, S::Auto2k), (CommonAspectRatio::Auto4k, S::Auto4k)] {
let request = GenerateImageRequest { aspect_ratio: Some(ar), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(
matches!(plan.image_size, Some(s) if std::mem::discriminant(&s) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, ar,
);
}
}
#[test]
fn image_size_unsupported_error_out() {
for ar in [CommonAspectRatio::WideFiveByFour, CommonAspectRatio::TallFourByFive] {
let request = GenerateImageRequest {
aspect_ratio: Some(ar),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_seedream_4p5_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
), "expected error for {:?}", ar);
}
}
#[test]
fn image_size_unsupported_nearest_match() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::WideFiveByFour),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::LandscapeSixteenNine)), "expected LandscapeSixteenNine with {:?}", strategy);
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallFourByFive),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.image_size, Some(S::PortraitSixteenNine)), "expected PortraitSixteenNine with {:?}", strategy);
}
}
// ── 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_seedream_4p5_image_request()
};
assert!(
matches!(request.build(), Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
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_seedream_4p5_image_request()
};
assert!(matches!(
request.build(),
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn num_images_out_of_range_clamps_to_four() {
for strategy in [RequestMismatchMitigationStrategy::PayMoreUpgrade, RequestMismatchMitigationStrategy::PayLessDowngrade] {
let request = GenerateImageRequest {
image_batch_count: Some(5),
request_mismatch_mitigation_strategy: strategy,
..base_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(matches!(plan.num_images, N::Four), "expected Four for count 5 with {:?}", strategy);
}
}
// ── Image inputs ──────────────────────────────────────────────────────────
#[test]
fn url_image_inputs_returns_error() {
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))
));
}
#[test]
fn no_image_inputs_is_text_to_image_mode() {
let request = GenerateImageRequest { image_inputs: None, ..base_seedream_4p5_image_request() };
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
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_seedream_4p5_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream4p5(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream4p5") };
assert!(plan.image_inputs.is_some());
}
}
@@ -1,211 +0,0 @@
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::image_list_ref::ImageListRef;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use artcraft_api_defs::generate::image::multi_function::bytedance_seedream_5_lite_multi_function_image_gen::{
BytedanceSeedream5LiteMultiFunctionImageGenImageSize,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
};
use tokens::tokens::media_files::MediaFileToken;
#[derive(Debug, Clone)]
pub struct PlanArtcraftSeedream5Lite<'a> {
pub prompt: Option<&'a str>,
/// Input images for image editing. None means text-to-image mode.
pub image_inputs: Option<&'a Vec<MediaFileToken>>,
pub image_size: Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>,
pub num_images: BytedanceSeedream5LiteMultiFunctionImageGenNumImages,
pub idempotency_token: String,
}
pub fn plan_generate_image_artcraft_seedream_5_lite<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanArtcraftSeedream5Lite<'a>, ArtcraftRouterError> {
let strategy = request.request_mismatch_mitigation_strategy;
let is_edit_mode = request.image_inputs.is_some();
let image_inputs = 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(PlanArtcraftSeedream5Lite {
prompt: request.prompt,
image_inputs,
image_size,
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)),
Some(ImageListRef::Urls(_)) => {
Err(ArtcraftRouterError::Client(ClientError::ArtcraftOnlySupportsMediaTokens))
}
}
}
// Seedream 5 Lite supported image sizes:
// Square, SquareHd
// PortraitFourThree, PortraitSixteenNine
// LandscapeFourThree, LandscapeSixteenNine
// Auto2k, Auto3k (NB: 5 Lite max is auto_3K, unlike V4.5's auto_4K)
fn plan_image_size(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<BytedanceSeedream5LiteMultiFunctionImageGenImageSize>, ArtcraftRouterError> {
use BytedanceSeedream5LiteMultiFunctionImageGenImageSize as S;
match aspect_ratio {
None => Ok(None),
// Auto: V5 has no bare Auto; fall back to Auto2k in edit mode, Square for text-to-image
Some(CommonAspectRatio::Auto) => {
if is_edit_mode { Ok(Some(S::Auto2k)) } else { Ok(Some(S::Square)) }
}
Some(CommonAspectRatio::Auto2k) => Ok(Some(S::Auto2k)),
// 5 Lite max is auto_3K; map Auto4k down to Auto3k
Some(CommonAspectRatio::Auto4k) => Ok(Some(S::Auto3k)),
// Square
Some(CommonAspectRatio::Square) => Ok(Some(S::Square)),
Some(CommonAspectRatio::SquareHd) => Ok(Some(S::SquareHd)),
// Wide — direct mappings
Some(CommonAspectRatio::Wide) | Some(CommonAspectRatio::WideSixteenByNine) => Ok(Some(S::LandscapeSixteenNine)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(S::LandscapeFourThree)),
// Wide — no direct equivalent, nearest is LandscapeSixteenNine
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),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::LandscapeSixteenNine)),
},
// Tall — direct mappings
Some(CommonAspectRatio::Tall) | Some(CommonAspectRatio::TallNineBySixteen) => Ok(Some(S::PortraitSixteenNine)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(S::PortraitFourThree)),
// Tall — no direct equivalent, nearest is PortraitSixteenNine
Some(unsupported @ CommonAspectRatio::TallFourByFive)
| Some(unsupported @ CommonAspectRatio::TallTwoByThree)
| Some(unsupported @ CommonAspectRatio::TallNineByTwentyOne) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(S::PortraitSixteenNine)),
},
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<BytedanceSeedream5LiteMultiFunctionImageGenNumImages, ArtcraftRouterError> {
use BytedanceSeedream5LiteMultiFunctionImageGenNumImages as N;
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(N::One),
2 => Ok(N::Two),
3 => Ok(N::Three),
4 => Ok(N::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(N::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::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::generate::generate_image::image_generation_plan::ImageGenerationPlan;
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,
BytedanceSeedream5LiteMultiFunctionImageGenNumImages as N,
};
#[test]
fn image_size_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_seedream_5_lite_image_request() };
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(plan.image_size.is_none());
}
#[test]
fn image_size_auto_in_edit_mode_yields_auto2k() {
let tokens = vec![];
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: Some(ImageListRef::MediaFileTokens(&tokens)),
..base_seedream_5_lite_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(matches!(plan.image_size, Some(S::Auto2k)));
}
#[test]
fn image_size_auto_in_text_to_image_yields_square() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto),
image_inputs: None,
..base_seedream_5_lite_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(matches!(plan.image_size, Some(S::Square)));
}
#[test]
fn auto4k_is_mapped_to_auto3k() {
// 5 Lite only supports up to auto_3K; auto_4K is downgraded
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::Auto4k),
..base_seedream_5_lite_image_request()
};
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(matches!(plan.image_size, Some(S::Auto3k)));
}
#[test]
fn num_images_direct_mapping() {
let cases = [(1, N::One), (2, N::Two), (3, N::Three), (4, N::Four)];
for (count, expected) in cases {
let request = GenerateImageRequest { image_batch_count: Some(count), ..base_seedream_5_lite_image_request() };
let ImageGenerationPlan::ArtcraftSeedream5Lite(plan) = request.build().unwrap() else { panic!("expected ArtcraftSeedream5Lite") };
assert!(
std::mem::discriminant(&plan.num_images) == std::mem::discriminant(&expected),
"expected {:?} for count {}", expected, count,
);
}
}
}
@@ -1 +0,0 @@
pub mod plan_generate_image_fal_nano_banana_pro;
@@ -1,531 +0,0 @@
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 fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::EnqueueNanoBananaProEditImageAspectRatio;
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::EnqueueNanoBananaProTextToImageAspectRatio;
/// Intermediate resolution type shared between t2i and edit enums
/// (both have the same variants: OneK, TwoK, FourK).
#[derive(Debug, Clone, Copy)]
pub enum FalNbpResolution {
OneK,
TwoK,
FourK,
}
/// Intermediate num-images type shared between t2i and edit enums
/// (both have the same variants: One, Two, Three, Four).
#[derive(Debug, Clone, Copy)]
pub enum FalNbpNumImages {
One,
Two,
Three,
Four,
}
#[derive(Debug, Clone)]
pub struct PlanFalNanaBananaPro<'a> {
pub prompt: Option<&'a str>,
/// Image URLs for editing. Empty vec = text-to-image mode.
pub image_urls: Vec<String>,
/// Pre-resolved aspect ratio for text-to-image mode.
pub t2i_aspect_ratio: Option<EnqueueNanoBananaProTextToImageAspectRatio>,
/// Pre-resolved aspect ratio for image-edit mode.
pub edit_aspect_ratio: Option<EnqueueNanoBananaProEditImageAspectRatio>,
/// Pre-resolved resolution (shared for both modes).
pub resolution: Option<FalNbpResolution>,
/// Pre-resolved number of images (1–4, shared for both modes).
pub num_images: FalNbpNumImages,
}
pub fn plan_generate_image_fal_nano_banana_pro<'a>(
request: &'a GenerateImageRequest<'a>,
) -> Result<PlanFalNanaBananaPro<'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 t2i_aspect_ratio = plan_t2i_aspect_ratio(request.aspect_ratio, strategy)?;
let edit_aspect_ratio = plan_edit_aspect_ratio(request.aspect_ratio, is_edit_mode, strategy)?;
let resolution = plan_resolution(request.resolution)?;
let num_images = plan_num_images(request.image_batch_count, strategy)?;
Ok(PlanFalNanaBananaPro {
prompt: request.prompt,
image_urls,
t2i_aspect_ratio,
edit_aspect_ratio,
resolution,
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))
}
}
}
// Nano Banana Pro text-to-image supported aspect ratios:
// OneByOne, FiveByFour, FourByThree, ThreeByTwo, SixteenByNine, TwentyOneByNine,
// FourByFive, ThreeByFour, TwoByThree, NineBySixteen
// (no Auto — that's edit-only)
fn plan_t2i_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<EnqueueNanoBananaProTextToImageAspectRatio>, ArtcraftRouterError> {
use EnqueueNanoBananaProTextToImageAspectRatio as T2iAr;
match aspect_ratio {
None => Ok(None),
// Auto not valid for text-to-image; fall back to square
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => Ok(Some(T2iAr::OneByOne)),
// Direct mappings
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(T2iAr::OneByOne)),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(T2iAr::FiveByFour)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(T2iAr::FourByThree)),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(T2iAr::ThreeByTwo)),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(T2iAr::SixteenByNine)),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(T2iAr::TwentyOneByNine)),
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(T2iAr::FourByFive)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(T2iAr::ThreeByFour)),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(T2iAr::TwoByThree)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(T2iAr::NineBySixteen)),
// Mismatch — TallNineByTwentyOne has no direct equivalent
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_t2i_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_t2i_aspect_ratio(
aspect_ratio: CommonAspectRatio,
) -> EnqueueNanoBananaProTextToImageAspectRatio {
use EnqueueNanoBananaProTextToImageAspectRatio as T2iAr;
match aspect_ratio {
// TallNineByTwentyOne (0.43) — nearest is NineBySixteen (0.5625)
CommonAspectRatio::TallNineByTwentyOne => T2iAr::NineBySixteen,
_ => T2iAr::OneByOne,
}
}
// Nano Banana Pro image-edit supported aspect ratios:
// Auto (use source image dims), plus all text-to-image ratios
fn plan_edit_aspect_ratio(
aspect_ratio: Option<CommonAspectRatio>,
is_edit_mode: bool,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<EnqueueNanoBananaProEditImageAspectRatio>, ArtcraftRouterError> {
use EnqueueNanoBananaProEditImageAspectRatio as EditAr;
match aspect_ratio {
None => Ok(None),
// Auto: valid in edit mode (use source image dimensions)
Some(CommonAspectRatio::Auto)
| Some(CommonAspectRatio::Auto2k)
| Some(CommonAspectRatio::Auto4k) => {
if is_edit_mode {
Ok(Some(EditAr::Auto))
} else {
Ok(Some(EditAr::OneByOne))
}
}
// Direct mappings
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => Ok(Some(EditAr::OneByOne)),
Some(CommonAspectRatio::WideFiveByFour) => Ok(Some(EditAr::FiveByFour)),
Some(CommonAspectRatio::WideFourByThree) => Ok(Some(EditAr::FourByThree)),
Some(CommonAspectRatio::WideThreeByTwo) => Ok(Some(EditAr::ThreeByTwo)),
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => Ok(Some(EditAr::SixteenByNine)),
Some(CommonAspectRatio::WideTwentyOneByNine) => Ok(Some(EditAr::TwentyOneByNine)),
Some(CommonAspectRatio::TallFourByFive) => Ok(Some(EditAr::FourByFive)),
Some(CommonAspectRatio::TallThreeByFour) => Ok(Some(EditAr::ThreeByFour)),
Some(CommonAspectRatio::TallTwoByThree) => Ok(Some(EditAr::TwoByThree)),
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => Ok(Some(EditAr::NineBySixteen)),
// Mismatch
Some(unsupported) => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "aspect_ratio",
value: format!("{:?}", unsupported),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade
| RequestMismatchMitigationStrategy::PayLessDowngrade => {
Ok(Some(nearest_edit_aspect_ratio(unsupported)))
}
},
}
}
fn nearest_edit_aspect_ratio(
aspect_ratio: CommonAspectRatio,
) -> EnqueueNanoBananaProEditImageAspectRatio {
use EnqueueNanoBananaProEditImageAspectRatio as EditAr;
match aspect_ratio {
CommonAspectRatio::TallNineByTwentyOne => EditAr::NineBySixteen,
_ => EditAr::OneByOne,
}
}
fn plan_resolution(
resolution: Option<CommonResolution>,
) -> Result<Option<FalNbpResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
Some(CommonResolution::OneK) => Ok(Some(FalNbpResolution::OneK)),
Some(CommonResolution::TwoK) => Ok(Some(FalNbpResolution::TwoK)),
Some(CommonResolution::FourK) => Ok(Some(FalNbpResolution::FourK)),
// No 3K option; fall back to 2K
Some(CommonResolution::ThreeK) => Ok(Some(FalNbpResolution::TwoK)),
}
}
fn plan_num_images(
image_batch_count: Option<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<FalNbpNumImages, ArtcraftRouterError> {
let count = image_batch_count.unwrap_or(1);
match count {
0 => Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations)),
1 => Ok(FalNbpNumImages::One),
2 => Ok(FalNbpNumImages::Two),
3 => Ok(FalNbpNumImages::Three),
4 => Ok(FalNbpNumImages::Four),
_ => match strategy {
RequestMismatchMitigationStrategy::ErrorOut => {
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_batch_count",
value: format!("{}", count),
}))
}
RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(FalNbpNumImages::Four),
RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(FalNbpNumImages::Four),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_resolution::CommonResolution;
use crate::api::image_list_ref::ImageListRef;
use crate::api::provider::Provider;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use fal_client::requests::webhook::image::edit::enqueue_nano_banana_pro_edit_image_webhook::EnqueueNanoBananaProEditImageAspectRatio as EditAr;
use fal_client::requests::webhook::image::text::enqueue_nano_banana_pro_text_to_image_webhook::EnqueueNanoBananaProTextToImageAspectRatio as T2iAr;
fn base_fal_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::NanaBananaPro,
provider: Provider::Fal,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
fn build_plan<'a>(
request: &'a GenerateImageRequest<'a>,
) -> PlanFalNanaBananaPro<'a> {
plan_generate_image_fal_nano_banana_pro(request).expect("plan should succeed")
}
// ── Image inputs / 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_nano_banana_pro(&request);
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::FalOnlySupportsUrls))
));
}
// ── Text-to-image aspect ratio ────────────────────────────────────────────────
#[test]
fn t2i_aspect_ratio_none_is_none() {
let request = GenerateImageRequest { aspect_ratio: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.t2i_aspect_ratio.is_none());
}
#[test]
fn t2i_aspect_ratio_auto_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_fal_request()
};
let plan = build_plan(&request);
assert!(
matches!(plan.t2i_aspect_ratio, Some(T2iAr::OneByOne)),
"expected OneByOne fallback in text-to-image for {:?}", auto_ar,
);
}
}
#[test]
fn t2i_aspect_ratio_direct_mappings() {
let cases = [
(CommonAspectRatio::Square, T2iAr::OneByOne),
(CommonAspectRatio::SquareHd, T2iAr::OneByOne),
(CommonAspectRatio::WideFiveByFour, T2iAr::FiveByFour),
(CommonAspectRatio::WideFourByThree, T2iAr::FourByThree),
(CommonAspectRatio::WideThreeByTwo, T2iAr::ThreeByTwo),
(CommonAspectRatio::WideSixteenByNine, T2iAr::SixteenByNine),
(CommonAspectRatio::Wide, T2iAr::SixteenByNine),
(CommonAspectRatio::WideTwentyOneByNine, T2iAr::TwentyOneByNine),
(CommonAspectRatio::TallFourByFive, T2iAr::FourByFive),
(CommonAspectRatio::TallThreeByFour, T2iAr::ThreeByFour),
(CommonAspectRatio::TallTwoByThree, T2iAr::TwoByThree),
(CommonAspectRatio::TallNineBySixteen, T2iAr::NineBySixteen),
(CommonAspectRatio::Tall, T2iAr::NineBySixteen),
];
for (common, expected) in cases {
let request = GenerateImageRequest { aspect_ratio: Some(common), ..base_fal_request() };
let plan = build_plan(&request);
assert!(
matches!(plan.t2i_aspect_ratio, Some(ar) if std::mem::discriminant(&ar) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn t2i_aspect_ratio_unsupported_error_out() {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
..base_fal_request()
};
let result = plan_generate_image_fal_nano_banana_pro(&request);
assert!(matches!(
result,
Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { .. }))
));
}
#[test]
fn t2i_aspect_ratio_unsupported_nearest_match() {
for strategy in [
RequestMismatchMitigationStrategy::PayMoreUpgrade,
RequestMismatchMitigationStrategy::PayLessDowngrade,
] {
let request = GenerateImageRequest {
aspect_ratio: Some(CommonAspectRatio::TallNineByTwentyOne),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
};
let plan = build_plan(&request);
assert!(
matches!(plan.t2i_aspect_ratio, Some(T2iAr::NineBySixteen)),
"expected NineBySixteen nearest match with {:?}", strategy,
);
}
}
// ── Edit aspect ratio ─────────────────────────────────────────────────────────
#[test]
fn edit_aspect_ratio_auto_in_edit_mode_yields_auto() {
let urls = vec!["https://example.com/img.jpg".to_string()];
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: Some(ImageListRef::Urls(&urls)),
..base_fal_request()
};
let plan = build_plan(&request);
assert!(
matches!(plan.edit_aspect_ratio, Some(EditAr::Auto)),
"expected Auto in edit mode for {:?}", auto_ar,
);
}
}
#[test]
fn edit_aspect_ratio_auto_in_text_to_image_falls_back_to_square() {
for auto_ar in [CommonAspectRatio::Auto, CommonAspectRatio::Auto2k, CommonAspectRatio::Auto4k] {
let request = GenerateImageRequest {
aspect_ratio: Some(auto_ar),
image_inputs: None,
..base_fal_request()
};
let plan = build_plan(&request);
assert!(
matches!(plan.edit_aspect_ratio, Some(EditAr::OneByOne)),
"expected OneByOne in text-to-image for {:?}", auto_ar,
);
}
}
// ── Resolution ────────────────────────────────────────────────────────────────
#[test]
fn resolution_none_is_none() {
let request = GenerateImageRequest { resolution: None, ..base_fal_request() };
let plan = build_plan(&request);
assert!(plan.resolution.is_none());
}
#[test]
fn resolution_direct_mappings() {
let cases = [
(CommonResolution::OneK, FalNbpResolution::OneK),
(CommonResolution::TwoK, FalNbpResolution::TwoK),
(CommonResolution::FourK, FalNbpResolution::FourK),
];
for (common, expected) in cases {
let request = GenerateImageRequest { resolution: Some(common), ..base_fal_request() };
let plan = build_plan(&request);
assert!(
matches!(plan.resolution, Some(r) if std::mem::discriminant(&r) == std::mem::discriminant(&expected)),
"expected {:?} for {:?}", expected, common,
);
}
}
#[test]
fn resolution_three_k_falls_back_to_two_k() {
let request = GenerateImageRequest {
resolution: Some(CommonResolution::ThreeK),
..base_fal_request()
};
let plan = build_plan(&request);
assert!(matches!(plan.resolution, Some(FalNbpResolution::TwoK)));
}
// ── 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_nano_banana_pro(&request);
assert!(
matches!(result, Err(ArtcraftRouterError::Client(ClientError::UserRequestedZeroGenerations))),
"expected UserRequestedZeroGenerations with {:?}", strategy,
);
}
}
#[test]
fn num_images_direct_mapping() {
let cases = [
(1, FalNbpNumImages::One),
(2, FalNbpNumImages::Two),
(3, FalNbpNumImages::Three),
(4, FalNbpNumImages::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_nano_banana_pro(&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(5),
request_mismatch_mitigation_strategy: strategy,
..base_fal_request()
};
let plan = build_plan(&request);
assert!(
matches!(plan.num_images, FalNbpNumImages::Four),
"expected Four for count 5 with {:?}", strategy,
);
}
}
}
@@ -1,2 +0,0 @@
pub mod artcraft;
pub mod fal;
@@ -1,96 +0,0 @@
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0BatchCount;
use seedance2pro::creds::seedance2pro_session::Seedance2ProSession;
use seedance2pro::requests::generate_video::generate_video::{BatchCount, GenerateVideoArgs, Resolution};
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
pub (crate) fn estimate_video_cost_artcraft_seedance2p0(
plan: &PlanArtcraftSeedance2p0<'_>,
) -> VideoGenerationCostEstimate {
let duration_seconds = plan.duration_seconds.unwrap_or(5).clamp(4, 15);
let batch_count = match plan.batch_count {
Seedance2p0BatchCount::One => BatchCount::One,
Seedance2p0BatchCount::Two => BatchCount::Two,
Seedance2p0BatchCount::Four => BatchCount::Four,
};
// TODO: Make a better client that doesn't require this.
// A dummy session is sufficient — cost estimation does not make any network calls.
let dummy_session = Seedance2ProSession::from_cookies_string(String::new());
let args = GenerateVideoArgs {
session: &dummy_session,
prompt: String::new(),
resolution: Resolution::Square1x1,
duration_seconds,
batch_count,
// NB: These do not contribute to costs in the Seedance2 integration
start_frame_url: None,
end_frame_url: None,
reference_image_urls: None,
};
let cost_in_usd_cents = args.estimate_cost_in_usd_cents();
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,
}
}
#[cfg(test)]
mod tests {
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
fn estimate_usd_cents(duration_seconds: u16, video_batch_count: u16) -> u64 {
let request = GenerateVideoRequest {
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: None,
start_frame: None,
end_frame: None,
reference_images: None,
resolution: None,
aspect_ratio: None,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
};
request.build()
.expect("build should succeed")
.estimate_costs()
.cost_in_usd_cents
.expect("cost_in_usd_cents should be present")
}
#[test]
fn test_estimate_cost_usd_cents() {
// Batch 1:
assert_eq!(estimate_usd_cents(4, 1), 64);
assert_eq!(estimate_usd_cents(5, 1), 80);
assert_eq!(estimate_usd_cents(6, 1), 96);
assert_eq!(estimate_usd_cents(7, 1), 112);
assert_eq!(estimate_usd_cents(15, 1), 240);
// Batch 2 = 2×
assert_eq!(estimate_usd_cents(4, 2), 128);
assert_eq!(estimate_usd_cents(5, 2), 160);
assert_eq!(estimate_usd_cents(15, 2), 480);
// Batch 4 = 3× (not 4×)
assert_eq!(estimate_usd_cents(4, 4), 192);
assert_eq!(estimate_usd_cents(5, 4), 240);
assert_eq!(estimate_usd_cents(15, 4), 720);
}
}
@@ -1 +0,0 @@
pub mod estimate_video_cost_artcraft_seedance2p0;
@@ -1 +0,0 @@
pub (crate) mod artcraft;
@@ -1,93 +0,0 @@
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::provider_error::ProviderError;
use crate::generate::generate_video::plan::artcraft::plan_generate_video_artcraft_seedance2p0::PlanArtcraftSeedance2p0;
use crate::generate::generate_video::generate_video_response::{
ArtcraftVideoResponsePayload, GenerateVideoResponse,
};
use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::Seedance2p0MultiFunctionVideoGenRequest;
use artcraft_client::endpoints::generate::video::multi_function::seedance_2p0_multi_function_video_gen::seedance_2p0_multi_function_video_gen;
pub async fn execute_artcraft_seedance2p0(
plan: &PlanArtcraftSeedance2p0<'_>,
artcraft_client: &RouterArtcraftClient,
) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
let request = Seedance2p0MultiFunctionVideoGenRequest {
uuid_idempotency_token: plan.idempotency_token.clone(),
prompt: plan.prompt.map(|p| p.to_string()),
start_frame_media_token: plan.start_frame.map(|t| t.to_owned()),
end_frame_media_token: plan.end_frame.map(|t| t.to_owned()),
reference_image_media_tokens: plan.reference_images.map(|tokens| tokens.to_owned()),
aspect_ratio: plan.aspect_ratio,
duration_seconds: plan.duration_seconds,
batch_count: Some(plan.batch_count),
};
let response = seedance_2p0_multi_function_video_gen(
&artcraft_client.api_host,
Some(&artcraft_client.credentials),
request,
)
.await
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Storyteller(err)))?;
Ok(GenerateVideoResponse::Artcraft(ArtcraftVideoResponsePayload {
inference_job_token: response.inference_job_token,
all_inference_job_tokens: response.all_inference_job_tokens,
}))
}
#[cfg(test)]
mod tests {
use crate::api::common_aspect_ratio::CommonAspectRatio;
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
use crate::test_helpers::{base_video_request, get_artcraft_client};
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_video_seedance_2p0() {
let client = get_artcraft_client();
let request = crate::generate::generate_video::generate_video_request::GenerateVideoRequest {
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
video_batch_count: Some(1),
prompt: Some("a cat walking through a cyberpunk city at night"),
..base_video_request()
};
let plan = request.build().unwrap();
let result = plan.generate_video(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_video request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job token: {:?}", payload.inference_job_token);
println!("All job tokens: {:?}", payload.all_inference_job_tokens);
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
#[tokio::test]
#[ignore] // manually run — fires a real API request and incurs cost
async fn test_text_to_video_seedance_2p0_batch_two() {
let client = get_artcraft_client();
let request = crate::generate::generate_video::generate_video_request::GenerateVideoRequest {
aspect_ratio: Some(CommonAspectRatio::Square),
video_batch_count: Some(2),
prompt: Some("a dog surfing a wave, cinematic"),
..base_video_request()
};
let plan = request.build().unwrap();
let result = plan.generate_video(&client).await;
println!("Result: {:?}", result);
let response = result.expect("generate_video request failed");
let payload = response.get_artcraft_payload().expect("expected Artcraft payload");
println!("Job tokens ({} total):", payload.all_inference_job_tokens.len());
for token in &payload.all_inference_job_tokens {
println!(" {:?}", token);
}
assert_eq!(1, 2); // NB: Intentional failure to inspect the response above.
}
}
@@ -1,20 +0,0 @@
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
#[derive(Clone, Debug)]
pub struct ArtcraftVideoResponsePayload {
pub inference_job_token: InferenceJobToken,
pub all_inference_job_tokens: Vec<InferenceJobToken>,
}
#[derive(Clone, Debug)]
pub enum GenerateVideoResponse {
Artcraft(ArtcraftVideoResponsePayload),
}
impl GenerateVideoResponse {
pub fn get_artcraft_payload(&self) -> Option<ArtcraftVideoResponsePayload> {
match self {
Self::Artcraft(p) => Some(p.clone()),
}
}
}
@@ -1,9 +0,0 @@
/// The result of estimating the cost of a video generation plan.
pub struct VideoGenerationCostEstimate {
pub cost_in_credits: Option<u64>,
pub cost_in_usd_cents: Option<u64>,
pub is_free: bool,
pub is_unlimited: bool,
pub is_rate_limited: bool,
pub has_watermark: bool,
}
@@ -1,227 +0,0 @@
use crate::api::common_image_model::CommonImageModel;
use crate::api::common_video_model::CommonVideoModel;
use crate::api::provider::Provider;
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
use crate::client::router_artcraft_client::RouterArtcraftClient;
use crate::client::router_client::RouterClient;
use crate::client::router_fal_client::RouterFalClient;
use crate::generate::generate_image::generate_image_request::GenerateImageRequest;
use crate::generate::generate_video::generate_video_request::GenerateVideoRequest;
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;
use fal_client::creds::fal_api_key::FalApiKey;
pub fn get_artcraft_client() -> RouterClient {
let cookies = std::fs::read_to_string("/Users/bt/Artcraft/credentials/artcraft_cookies.txt")
.expect("Failed to read /Users/bt/Artcraft/credentials/artcraft_cookies.txt");
let cookies = cookies.trim().to_string();
let credentials = StorytellerCredentialSet::parse_multi_cookie_header(&cookies)
.expect("Failed to parse cookies")
.expect("No credentials found");
RouterClient::Artcraft(RouterArtcraftClient::new(ApiHost::Storyteller, credentials))
}
pub fn base_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::NanaBananaPro,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn get_fal_client() -> RouterClient {
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/fal_api_key.txt")
.expect("Failed to read /Users/bt/Artcraft/credentials/fal_api_key.txt");
let api_key = FalApiKey::from_str(secret.trim());
let webhook_url = "https://example.com/fal-webhook-test".to_string();
RouterClient::Fal(RouterFalClient::new(api_key, webhook_url))
}
pub fn base_fal_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::NanaBananaPro,
provider: Provider::Fal,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_seedream_4_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Seedream4,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_seedream_4p5_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Seedream4p5,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_seedream_5_lite_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Seedream5Lite,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_nano_banana_2_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::NanaBanana2,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_nano_banana_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::NanaBanana,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_gpt_image_1p5_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::GptImage1p5,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_flux_1_dev_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Flux1Dev,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_flux_1_schnell_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::Flux1Schnell,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_flux_pro_1p1_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::FluxPro11,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_flux_pro_1p1_ultra_image_request() -> GenerateImageRequest<'static> {
GenerateImageRequest {
model: CommonImageModel::FluxPro11Ultra,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
image_inputs: None,
resolution: None,
aspect_ratio: None,
image_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
generation_mode_mismatch_strategy: None,
idempotency_token: None,
}
}
pub fn base_video_request() -> GenerateVideoRequest<'static> {
GenerateVideoRequest {
model: CommonVideoModel::Seedance2p0,
provider: Provider::Artcraft,
prompt: Some("a cat in space"),
start_frame: None,
end_frame: None,
reference_images: None,
resolution: None,
aspect_ratio: None,
duration_seconds: None,
video_batch_count: None,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut,
idempotency_token: None,
}
}
@@ -1,5 +1,5 @@
[package]
name = "artcraft_client"
name = "storyteller_client"
edition = "2021"
version = "0.0.1"
authors = [
@@ -15,7 +15,7 @@ normal = ["workspace-build-acceleration"]
# unused_imports = "forbid"
[lib]
name = "artcraft_client"
name = "storyteller_client"
path = "src/lib.rs"
[dependencies]

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