artcraft_router artcraft

This commit is contained in:
Brandon Thomas committed 2026-06-02 05:13:01 -04:00
1 parent 78899550cd
commit 6e3074d4e6
15 files changed
+565

No files matched your search

@@ -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),
@@ -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) => {
@@ -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<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney7)?;
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::ArtcraftMidjourney7(ArtcraftMidjourney7RequestState { request }),
))
}
@@ -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);
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateImageResponse, ArtcraftRouterError> {
send_artcraft_omni_image_request(&self.request, client).await
}
}
@@ -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<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney7Niji)?;
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::ArtcraftMidjourney7Niji(ArtcraftMidjourney7NijiRequestState { request }),
))
}
@@ -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());
}
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateImageResponse, ArtcraftRouterError> {
send_artcraft_omni_image_request(&self.request, client).await
}
}
@@ -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<ImageGenerationDraftOrRequest, ArtcraftRouterError> {
let request = build_artcraft_omni_image_request(builder, CommonImageModelEnum::Midjourney8)?;
Ok(ImageGenerationDraftOrRequest::Request(
ImageGenerationRequest::ArtcraftMidjourney8(ArtcraftMidjourney8RequestState { request }),
))
}
@@ -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());
}
}
}
@@ -0,0 +1,3 @@
pub mod build;
pub mod cost;
pub mod request;
@@ -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<GenerateImageResponse, ArtcraftRouterError> {
send_artcraft_omni_image_request(&self.request, client).await
}
}
@@ -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;