From 6e3074d4e6fa198d30e59364cca7d7db63a02728 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 2 Jun 2026 05:13:01 -0400 Subject: [PATCH] artcraft_router artcraft --- .../generate_image_request_builder.rs | 6 + .../image_generation_request.rs | 33 ++++ .../providers/artcraft/midjourney_7/build.rs | 17 ++ .../providers/artcraft/midjourney_7/cost.rs | 186 ++++++++++++++++++ .../providers/artcraft/midjourney_7/mod.rs | 3 + .../artcraft/midjourney_7/request.rs | 17 ++ .../artcraft/midjourney_7_niji/build.rs | 17 ++ .../artcraft/midjourney_7_niji/cost.rs | 113 +++++++++++ .../artcraft/midjourney_7_niji/mod.rs | 3 + .../artcraft/midjourney_7_niji/request.rs | 17 ++ .../providers/artcraft/midjourney_8/build.rs | 17 ++ .../providers/artcraft/midjourney_8/cost.rs | 113 +++++++++++ .../providers/artcraft/midjourney_8/mod.rs | 3 + .../artcraft/midjourney_8/request.rs | 17 ++ .../generate_image/providers/artcraft/mod.rs | 3 + 15 files changed, 565 insertions(+) create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/request.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/build.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/mod.rs create mode 100644 crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/request.rs diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs index 8df5fecb11..00b7956cac 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/generate_image_request_builder.rs @@ -16,6 +16,9 @@ use crate::generate::generate_image::providers::artcraft::flux_pro_1p1_ultra::bu use crate::generate::generate_image::providers::artcraft::gpt_image_1::build::build_artcraft_gpt_image_1; use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::build::build_artcraft_gpt_image_1p5; use crate::generate::generate_image::providers::artcraft::gpt_image_2::build::build_artcraft_gpt_image_2; +use crate::generate::generate_image::providers::artcraft::midjourney_7::build::build_artcraft_midjourney_7; +use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::build::build_artcraft_midjourney_7_niji; +use crate::generate::generate_image::providers::artcraft::midjourney_8::build::build_artcraft_midjourney_8; use crate::generate::generate_image::providers::artcraft::nano_banana::build::build_artcraft_nano_banana; use crate::generate::generate_image::providers::artcraft::nano_banana_2::build::build_artcraft_nano_banana_2; use crate::generate::generate_image::providers::artcraft::nano_banana_pro::build::build_artcraft_nano_banana_pro; @@ -118,6 +121,9 @@ impl GenerateImageRequestBuilder { (RouterProvider::Artcraft, RouterImageModel::Seedream5Lite) => build_artcraft_seedream_5_lite(self), (RouterProvider::Artcraft, RouterImageModel::QwenEdit2511Angles) => build_artcraft_qwen_edit_2511_angles(self), (RouterProvider::Artcraft, RouterImageModel::Flux2LoraAngles) => build_artcraft_flux_2_lora_angles(self), + (RouterProvider::Artcraft, RouterImageModel::Midjourney7) => build_artcraft_midjourney_7(self), + (RouterProvider::Artcraft, RouterImageModel::Midjourney7Niji) => build_artcraft_midjourney_7_niji(self), + (RouterProvider::Artcraft, RouterImageModel::Midjourney8) => build_artcraft_midjourney_8(self), (RouterProvider::Fal, RouterImageModel::Flux1Dev) => build_fal_flux_1_dev(self), (RouterProvider::Fal, RouterImageModel::Flux1Schnell) => build_fal_flux_1_schnell(self), diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_request.rs index e23a30d050..5714c4b439 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_request.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/image_generation_request.rs @@ -19,6 +19,12 @@ use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::cost::A use crate::generate::generate_image::providers::artcraft::gpt_image_1p5::request::ArtcraftGptImage1p5RequestState; use crate::generate::generate_image::providers::artcraft::gpt_image_2::cost::ArtcraftGptImage2CostState; use crate::generate::generate_image::providers::artcraft::gpt_image_2::request::ArtcraftGptImage2RequestState; +use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState; +use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState; +use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::cost::ArtcraftMidjourney7NijiCostState; +use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState; +use crate::generate::generate_image::providers::artcraft::midjourney_8::cost::ArtcraftMidjourney8CostState; +use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState; use crate::generate::generate_image::providers::artcraft::nano_banana::cost::ArtcraftNanoBananaCostState; use crate::generate::generate_image::providers::artcraft::nano_banana::request::ArtcraftNanoBananaRequestState; use crate::generate::generate_image::providers::artcraft::nano_banana_2::cost::ArtcraftNanoBanana2CostState; @@ -88,6 +94,9 @@ pub enum ImageGenerationRequest { ArtcraftSeedream5Lite(ArtcraftSeedream5LiteRequestState), ArtcraftQwenEdit2511Angles(ArtcraftQwenEdit2511AnglesRequestState), ArtcraftFlux2LoraAngles(ArtcraftFlux2LoraAnglesRequestState), + ArtcraftMidjourney7(ArtcraftMidjourney7RequestState), + ArtcraftMidjourney7Niji(ArtcraftMidjourney7NijiRequestState), + ArtcraftMidjourney8(ArtcraftMidjourney8RequestState), // ── Fal provider ── FalFlux1Dev(FalFlux1DevRequestState), @@ -130,6 +139,9 @@ impl ImageGenerationRequest { Self::ArtcraftSeedream5Lite(_) => RouterProvider::Artcraft, Self::ArtcraftQwenEdit2511Angles(_) => RouterProvider::Artcraft, Self::ArtcraftFlux2LoraAngles(_) => RouterProvider::Artcraft, + Self::ArtcraftMidjourney7(_) => RouterProvider::Artcraft, + Self::ArtcraftMidjourney7Niji(_) => RouterProvider::Artcraft, + Self::ArtcraftMidjourney8(_) => RouterProvider::Artcraft, Self::FalFlux1Dev(_) => RouterProvider::Fal, Self::FalFlux1Schnell(_) => RouterProvider::Fal, @@ -201,6 +213,15 @@ impl ImageGenerationRequest { Self::ArtcraftFlux2LoraAngles(request) => { Ok(ArtcraftFlux2LoraAnglesCostState::from_request(request).estimate_cost()) } + Self::ArtcraftMidjourney7(request) => { + Ok(ArtcraftMidjourney7CostState::from_request(request).estimate_cost()) + } + Self::ArtcraftMidjourney7Niji(request) => { + Ok(ArtcraftMidjourney7NijiCostState::from_request(request).estimate_cost()) + } + Self::ArtcraftMidjourney8(request) => { + Ok(ArtcraftMidjourney8CostState::from_request(request).estimate_cost()) + } // ── Fal ── Self::FalFlux1Dev(request) => Ok(FalFlux1DevCostState::from_request(request).estimate_cost()), @@ -288,6 +309,18 @@ impl ImageGenerationRequest { let artcraft_client = client.get_artcraft_client_ref()?; request.send(artcraft_client).await } + Self::ArtcraftMidjourney7(request) => { + let artcraft_client = client.get_artcraft_client_ref()?; + request.send(artcraft_client).await + } + Self::ArtcraftMidjourney7Niji(request) => { + let artcraft_client = client.get_artcraft_client_ref()?; + request.send(artcraft_client).await + } + Self::ArtcraftMidjourney8(request) => { + let artcraft_client = client.get_artcraft_client_ref()?; + request.send(artcraft_client).await + } // ── Fal ── Self::FalFlux1Dev(request) => { diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/build.rs new file mode 100644 index 0000000000..aea16c0779 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/build.rs @@ -0,0 +1,17 @@ +use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum; + +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; +use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest; +use crate::generate::generate_image::image_generation_request::ImageGenerationRequest; +use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request; +use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState; + +pub fn build_artcraft_midjourney_7( + builder: GenerateImageRequestBuilder, +) -> Result { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney7)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftMidjourney7(ArtcraftMidjourney7RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs new file mode 100644 index 0000000000..3d5116afbc --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/cost.rs @@ -0,0 +1,186 @@ +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::providers::artcraft::midjourney_7::request::ArtcraftMidjourney7RequestState; + +/// Cost state for Artcraft Midjourney 7. Pricing mirrors the Kinovi +/// midjourney_7 provider 1:1 in USD cents — ArtCraft credits map cent-for-cent. +/// +/// | batch | credits | USD cents | +/// |-------|---------|-----------| +/// | 1 | 6 | 6¢ | +/// | 2 | 12 | 12¢ | +/// | 3 | 19 | 19¢ | (interpolated; Kinovi doesn't expose 3) +/// | 4 | 25 | 25¢ | +/// +/// Kinovi-side math: 12 Kinovi credits per Midjourney task; 193 Kinovi +/// credits per USD. Kinovi rounds to nearest cent, so batches 1/2/4 land +/// on 6/12/25. Batch 3 is `ceil((12 × 3 / 193) × 100) = 19`. +#[derive(Clone, Debug)] +pub struct ArtcraftMidjourney7CostState { + pub num_images: u16, +} + +impl ArtcraftMidjourney7CostState { + pub fn from_request(request: &ArtcraftMidjourney7RequestState) -> Self { + Self { + num_images: request.request.image_batch_count.unwrap_or(1), + } + } + + pub fn estimate_cost(&self) -> ImageGenerationCostEstimate { + let cost_in_usd_cents = midjourney_cost_for_batch(self.num_images); + ImageGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +/// Returns the customer-facing midjourney cost in USD cents for a given +/// batch count. Shared between v7, v7-niji, and v8 (all three Midjourney +/// models have identical pricing on Kinovi). +pub(crate) fn midjourney_cost_for_batch(batch_count: u16) -> u64 { + match batch_count { + 0 => 0, // Defensive; the builder rejects zero before reaching cost. + 1 => 6, + 2 => 12, + 3 => 19, + 4 => 25, + // Should never happen — `plan_batch_count` clamps to 1..=4. + _ => 25, + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::router_image_model::RouterImageModel; + use crate::api::router_provider::RouterProvider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; + + fn cost_cents(image_batch_count: u16) -> u64 { + let builder = GenerateImageRequestBuilder { + model: RouterImageModel::Midjourney7, + provider: RouterProvider::Artcraft, + prompt: Some("a corgi astronaut".to_string()), + image_inputs: None, + resolution: None, + aspect_ratio: None, + quality: None, + image_batch_count: Some(image_batch_count), + horizontal_angle: None, + vertical_angle: None, + zoom: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + generation_mode_mismatch_strategy: None, + idempotency_token: None, + }; + builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + fn cost_credits(image_batch_count: u16) -> u64 { + let builder = GenerateImageRequestBuilder { + model: RouterImageModel::Midjourney7, + provider: RouterProvider::Artcraft, + prompt: Some("a corgi astronaut".to_string()), + image_inputs: None, + resolution: None, + aspect_ratio: None, + quality: None, + image_batch_count: Some(image_batch_count), + horizontal_angle: None, + vertical_angle: None, + zoom: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + generation_mode_mismatch_strategy: None, + idempotency_token: None, + }; + builder.build2().unwrap().estimate_cost().unwrap().cost_in_credits.unwrap() + } + + // ── Spot checks: each supported batch size ── + + #[test] + fn one_image_is_six_cents() { assert_eq!(cost_cents(1), 6); } + + #[test] + fn two_images_is_twelve_cents() { assert_eq!(cost_cents(2), 12); } + + #[test] + fn three_images_is_nineteen_cents() { assert_eq!(cost_cents(3), 19); } + + #[test] + fn four_images_is_twentyfive_cents() { assert_eq!(cost_cents(4), 25); } + + // ── Credits track USD cents exactly (1 credit = 1 cent) ── + + #[test] + fn credits_equal_usd_cents() { + for batch in 1..=4u16 { + assert_eq!(cost_credits(batch), cost_cents(batch), + "credits should equal usd cents at batch {}", batch); + } + } + + // ── 1:1 cost parity with Kinovi for batches 1/2/4 ── + // + // Kinovi midjourney_7 returns 6/12/25¢ for batches 1/2/4. The artcraft + // provider must match those values byte-for-byte so users pay the same. + + mod kinovi_parity { + use super::*; + use crate::generate::generate_image::providers::kinovi::midjourney_7::cost::KinoviMidjourney7CostState; + use seedance2pro_client::generate::image::generate_midjourney_v7::KinoviMidjourneyBatchCount; + + #[test] + fn batch_1_matches_kinovi() { + let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::One } + .estimate_cost(); + assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap()); + } + + #[test] + fn batch_2_matches_kinovi() { + let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Two } + .estimate_cost(); + assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap()); + } + + #[test] + fn batch_4_matches_kinovi() { + let kinovi = KinoviMidjourney7CostState { batch_count: KinoviMidjourneyBatchCount::Four } + .estimate_cost(); + assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap()); + } + } + + // ── Monotonicity ── + + #[test] + fn cost_is_monotonically_increasing() { + let c1 = cost_cents(1); + let c2 = cost_cents(2); + let c3 = cost_cents(3); + let c4 = cost_cents(4); + assert!(c1 < c2, "{} < {}", c1, c2); + assert!(c2 < c3, "{} < {}", c2, c3); + assert!(c3 < c4, "{} < {}", c3, c4); + } + + // ── Flags ── + + #[test] + fn cost_flags_are_correct() { + let cost = ArtcraftMidjourney7CostState { num_images: 1 }.estimate_cost(); + assert!(!cost.is_free); + assert!(!cost.is_unlimited); + assert!(!cost.is_rate_limited); + assert!(!cost.has_watermark); + assert_eq!(cost.failures_are_refunded, None); + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/request.rs new file mode 100644 index 0000000000..ab97947edc --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7/request.rs @@ -0,0 +1,17 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; + +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::generate_image_response::GenerateImageResponse; +use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request; + +#[derive(Clone, Debug)] +pub struct ArtcraftMidjourney7RequestState { + pub request: OmniGenImageCostAndGenerateRequest, +} + +impl ArtcraftMidjourney7RequestState { + pub async fn send(&self, client: &RouterArtcraftClient) -> Result { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/build.rs new file mode 100644 index 0000000000..d5d3e0e945 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/build.rs @@ -0,0 +1,17 @@ +use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum; + +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; +use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest; +use crate::generate::generate_image::image_generation_request::ImageGenerationRequest; +use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request; +use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState; + +pub fn build_artcraft_midjourney_7_niji( + builder: GenerateImageRequestBuilder, +) -> Result { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney7Niji)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftMidjourney7Niji(ArtcraftMidjourney7NijiRequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs new file mode 100644 index 0000000000..cc8fd55ca1 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/cost.rs @@ -0,0 +1,113 @@ +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::midjourney_cost_for_batch; +use crate::generate::generate_image::providers::artcraft::midjourney_7_niji::request::ArtcraftMidjourney7NijiRequestState; + +/// Cost state for Artcraft Midjourney 7 Niji. Pricing is identical to v7 +/// and v8 on Kinovi, so we share the per-batch table via +/// `midjourney_cost_for_batch`. +#[derive(Clone, Debug)] +pub struct ArtcraftMidjourney7NijiCostState { + pub num_images: u16, +} + +impl ArtcraftMidjourney7NijiCostState { + pub fn from_request(request: &ArtcraftMidjourney7NijiRequestState) -> Self { + Self { + num_images: request.request.image_batch_count.unwrap_or(1), + } + } + + pub fn estimate_cost(&self) -> ImageGenerationCostEstimate { + let cost_in_usd_cents = midjourney_cost_for_batch(self.num_images); + ImageGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::router_image_model::RouterImageModel; + use crate::api::router_provider::RouterProvider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; + + fn cost_cents(image_batch_count: u16) -> u64 { + let builder = GenerateImageRequestBuilder { + model: RouterImageModel::Midjourney7Niji, + provider: RouterProvider::Artcraft, + prompt: Some("anime forest".to_string()), + image_inputs: None, + resolution: None, + aspect_ratio: None, + quality: None, + image_batch_count: Some(image_batch_count), + horizontal_angle: None, + vertical_angle: None, + zoom: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + generation_mode_mismatch_strategy: None, + idempotency_token: None, + }; + builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn batch_1_is_6() { assert_eq!(cost_cents(1), 6); } + + #[test] + fn batch_2_is_12() { assert_eq!(cost_cents(2), 12); } + + #[test] + fn batch_3_is_19() { assert_eq!(cost_cents(3), 19); } + + #[test] + fn batch_4_is_25() { assert_eq!(cost_cents(4), 25); } + + #[test] + fn niji_pricing_matches_v7() { + use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState; + for batch in 1..=4u16 { + let v7 = ArtcraftMidjourney7CostState { num_images: batch }.estimate_cost(); + let niji = ArtcraftMidjourney7NijiCostState { num_images: batch }.estimate_cost(); + assert_eq!(v7.cost_in_usd_cents, niji.cost_in_usd_cents, "batch {}", batch); + assert_eq!(v7.cost_in_credits, niji.cost_in_credits, "batch {}", batch); + } + } + + // ── 1:1 cost parity with Kinovi 7-niji ── + + mod kinovi_parity { + use super::*; + use crate::generate::generate_image::providers::kinovi::midjourney_7_niji::cost::KinoviMidjourney7NijiCostState; + use seedance2pro_client::generate::image::generate_midjourney_v7_niji::KinoviMidjourneyBatchCount; + + #[test] + fn batch_1_matches_kinovi() { + let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::One } + .estimate_cost(); + assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap()); + } + + #[test] + fn batch_2_matches_kinovi() { + let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Two } + .estimate_cost(); + assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap()); + } + + #[test] + fn batch_4_matches_kinovi() { + let kinovi = KinoviMidjourney7NijiCostState { batch_count: KinoviMidjourneyBatchCount::Four } + .estimate_cost(); + assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap()); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/request.rs new file mode 100644 index 0000000000..d56f3291ce --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_7_niji/request.rs @@ -0,0 +1,17 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; + +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::generate_image_response::GenerateImageResponse; +use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request; + +#[derive(Clone, Debug)] +pub struct ArtcraftMidjourney7NijiRequestState { + pub request: OmniGenImageCostAndGenerateRequest, +} + +impl ArtcraftMidjourney7NijiRequestState { + pub async fn send(&self, client: &RouterArtcraftClient) -> Result { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/build.rs new file mode 100644 index 0000000000..825ecced6a --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/build.rs @@ -0,0 +1,17 @@ +use enums::common::generation::common_image_model::CommonImageModel as CommonImageModelEnum; + +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; +use crate::generate::generate_image::image_generation_draft_or_request::ImageGenerationDraftOrRequest; +use crate::generate::generate_image::image_generation_request::ImageGenerationRequest; +use crate::generate::generate_image::providers::artcraft::build_common::build_artcraft_omni_image_request; +use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState; + +pub fn build_artcraft_midjourney_8( + builder: GenerateImageRequestBuilder, +) -> Result { + let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney8)?; + Ok(ImageGenerationDraftOrRequest::Request( + ImageGenerationRequest::ArtcraftMidjourney8(ArtcraftMidjourney8RequestState { request }), + )) +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs new file mode 100644 index 0000000000..6ab6e0759f --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/cost.rs @@ -0,0 +1,113 @@ +use crate::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate; +use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::midjourney_cost_for_batch; +use crate::generate::generate_image::providers::artcraft::midjourney_8::request::ArtcraftMidjourney8RequestState; + +/// Cost state for Artcraft Midjourney 8. Pricing is identical to v7 and +/// v7-niji on Kinovi; shares the per-batch table via +/// `midjourney_cost_for_batch`. +#[derive(Clone, Debug)] +pub struct ArtcraftMidjourney8CostState { + pub num_images: u16, +} + +impl ArtcraftMidjourney8CostState { + pub fn from_request(request: &ArtcraftMidjourney8RequestState) -> Self { + Self { + num_images: request.request.image_batch_count.unwrap_or(1), + } + } + + pub fn estimate_cost(&self) -> ImageGenerationCostEstimate { + let cost_in_usd_cents = midjourney_cost_for_batch(self.num_images); + ImageGenerationCostEstimate { + cost_in_credits: Some(cost_in_usd_cents), + cost_in_usd_cents: Some(cost_in_usd_cents), + is_free: false, + is_unlimited: false, + is_rate_limited: false, + has_watermark: false, + failures_are_refunded: None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::api::router_image_model::RouterImageModel; + use crate::api::router_provider::RouterProvider; + use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy; + use crate::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder; + + fn cost_cents(image_batch_count: u16) -> u64 { + let builder = GenerateImageRequestBuilder { + model: RouterImageModel::Midjourney8, + provider: RouterProvider::Artcraft, + prompt: Some("a haunted mansion".to_string()), + image_inputs: None, + resolution: None, + aspect_ratio: None, + quality: None, + image_batch_count: Some(image_batch_count), + horizontal_angle: None, + vertical_angle: None, + zoom: None, + request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::ErrorOut, + generation_mode_mismatch_strategy: None, + idempotency_token: None, + }; + builder.build2().unwrap().estimate_cost().unwrap().cost_in_usd_cents.unwrap() + } + + #[test] + fn batch_1_is_6() { assert_eq!(cost_cents(1), 6); } + + #[test] + fn batch_2_is_12() { assert_eq!(cost_cents(2), 12); } + + #[test] + fn batch_3_is_19() { assert_eq!(cost_cents(3), 19); } + + #[test] + fn batch_4_is_25() { assert_eq!(cost_cents(4), 25); } + + #[test] + fn v8_pricing_matches_v7() { + use crate::generate::generate_image::providers::artcraft::midjourney_7::cost::ArtcraftMidjourney7CostState; + for batch in 1..=4u16 { + let v7 = ArtcraftMidjourney7CostState { num_images: batch }.estimate_cost(); + let v8 = ArtcraftMidjourney8CostState { num_images: batch }.estimate_cost(); + assert_eq!(v7.cost_in_usd_cents, v8.cost_in_usd_cents, "batch {}", batch); + assert_eq!(v7.cost_in_credits, v8.cost_in_credits, "batch {}", batch); + } + } + + // ── 1:1 cost parity with Kinovi v8 ── + + mod kinovi_parity { + use super::*; + use crate::generate::generate_image::providers::kinovi::midjourney_8::cost::KinoviMidjourney8CostState; + use seedance2pro_client::generate::image::generate_midjourney_v8::KinoviMidjourneyBatchCount; + + #[test] + fn batch_1_matches_kinovi() { + let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::One } + .estimate_cost(); + assert_eq!(cost_cents(1), kinovi.cost_in_usd_cents.unwrap()); + } + + #[test] + fn batch_2_matches_kinovi() { + let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Two } + .estimate_cost(); + assert_eq!(cost_cents(2), kinovi.cost_in_usd_cents.unwrap()); + } + + #[test] + fn batch_4_matches_kinovi() { + let kinovi = KinoviMidjourney8CostState { batch_count: KinoviMidjourneyBatchCount::Four } + .estimate_cost(); + assert_eq!(cost_cents(4), kinovi.cost_in_usd_cents.unwrap()); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/mod.rs new file mode 100644 index 0000000000..98150bd014 --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/mod.rs @@ -0,0 +1,3 @@ +pub mod build; +pub mod cost; +pub mod request; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/request.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/request.rs new file mode 100644 index 0000000000..1f0bd730bf --- /dev/null +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/midjourney_8/request.rs @@ -0,0 +1,17 @@ +use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest; + +use crate::client::router_artcraft_client::RouterArtcraftClient; +use crate::errors::artcraft_router_error::ArtcraftRouterError; +use crate::generate::generate_image::generate_image_response::GenerateImageResponse; +use crate::generate::generate_image::providers::artcraft::request_common::send_artcraft_omni_image_request; + +#[derive(Clone, Debug)] +pub struct ArtcraftMidjourney8RequestState { + pub request: OmniGenImageCostAndGenerateRequest, +} + +impl ArtcraftMidjourney8RequestState { + pub async fn send(&self, client: &RouterArtcraftClient) -> Result { + send_artcraft_omni_image_request(&self.request, client).await + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/mod.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/mod.rs index 129daf541d..727384ef8a 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/mod.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_image/providers/artcraft/mod.rs @@ -8,6 +8,9 @@ pub mod flux_pro_1p1_ultra; pub mod gpt_image_1; pub mod gpt_image_1p5; pub mod gpt_image_2; +pub mod midjourney_7; +pub mod midjourney_7_niji; +pub mod midjourney_8; pub mod nano_banana; pub mod nano_banana_2; pub mod nano_banana_pro;