From 41d209d46f47f80559daf8ab0d3ddac7b4701150 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 7 Jul 2026 20:28:35 -0400 Subject: [PATCH] fix models --- .../providers/artcraft/kling_1_6_pro/build.rs | 69 ++++++++++++++++++- .../providers/artcraft/kling_1_6_pro/cost.rs | 10 +-- .../artcraft/seedance_1p0_lite/build.rs | 3 +- .../artcraft/seedance_1p0_lite/cost.rs | 34 +++++---- 4 files changed, 95 insertions(+), 21 deletions(-) diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/build.rs index 9d1a483547..670e694504 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/build.rs @@ -1,6 +1,8 @@ use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum; +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_video::generate_video_request_builder::GenerateVideoRequestBuilder; use crate::generate::generate_video::providers::artcraft::build_common::{ build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport, @@ -9,7 +11,13 @@ use crate::generate::generate_video::providers::artcraft::kling_1_6_pro::request use crate::generate::generate_video::video_generation_draft_or_request::VideoGenerationDraftOrRequest; use crate::generate::generate_video::video_generation_request::VideoGenerationRequest; -pub fn build_artcraft_kling_1_6_pro(builder: GenerateVideoRequestBuilder) -> Result { +pub fn build_artcraft_kling_1_6_pro(mut builder: GenerateVideoRequestBuilder) -> Result { + // Kling 1.6 Pro serves only {5, 10} seconds. Pre-plan the duration with the + // SAME normalization the dispatching provider uses, so the billed duration + // always matches the generated one. + let strategy = builder.request_mismatch_mitigation_strategy; + builder.duration_seconds = plan_kling_1_6_pro_duration(builder.duration_seconds, strategy)?; + let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Kling16Pro, @@ -19,3 +27,62 @@ pub fn build_artcraft_kling_1_6_pro(builder: GenerateVideoRequestBuilder) -> Res let state = ArtcraftKling16ProRequestState { request }; Ok(VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::ArtcraftKling16Pro(state))) } + +/// Mirrors the Fal-side `plan_duration` for Kling 1.6 Pro: unsupported +/// durations upgrade to 10s (PayMoreUpgrade) or downgrade to 5s +/// (PayLessDowngrade). +fn plan_kling_1_6_pro_duration( + duration_seconds: Option, + strategy: RequestMismatchMitigationStrategy, +) -> Result, ArtcraftRouterError> { + match duration_seconds { + None => Ok(None), + Some(5) | Some(10) => Ok(duration_seconds), + Some(other) => match strategy { + RequestMismatchMitigationStrategy::ErrorOut => { + Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { + field: "duration_seconds", + value: format!("{}", other), + })) + } + RequestMismatchMitigationStrategy::PayMoreUpgrade => Ok(Some(10)), + RequestMismatchMitigationStrategy::PayLessDowngrade => Ok(Some(5)), + }, + } +} + +#[cfg(test)] +mod tests { + use super::*; + + mod duration_planning { + use super::*; + + #[test] + fn supported_durations_pass_through() { + let s = RequestMismatchMitigationStrategy::PayMoreUpgrade; + assert_eq!(plan_kling_1_6_pro_duration(None, s).unwrap(), None); + assert_eq!(plan_kling_1_6_pro_duration(Some(5), s).unwrap(), Some(5)); + assert_eq!(plan_kling_1_6_pro_duration(Some(10), s).unwrap(), Some(10)); + } + + #[test] + fn unsupported_duration_upgrades_to_ten() { + let s = RequestMismatchMitigationStrategy::PayMoreUpgrade; + assert_eq!(plan_kling_1_6_pro_duration(Some(4), s).unwrap(), Some(10)); + assert_eq!(plan_kling_1_6_pro_duration(Some(7), s).unwrap(), Some(10)); + } + + #[test] + fn unsupported_duration_downgrades_to_five() { + let s = RequestMismatchMitigationStrategy::PayLessDowngrade; + assert_eq!(plan_kling_1_6_pro_duration(Some(4), s).unwrap(), Some(5)); + } + + #[test] + fn unsupported_duration_errors_in_strict_mode() { + let s = RequestMismatchMitigationStrategy::ErrorOut; + assert!(plan_kling_1_6_pro_duration(Some(4), s).is_err()); + } + } +} diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/cost.rs index f5c93a18b0..9d67161659 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/kling_1_6_pro/cost.rs @@ -15,8 +15,8 @@ impl ArtcraftKling16ProCostState { } pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { - // Mirrors fal_client kling_v1p6_pro: 5s = 48¢, 10s = 95¢. - let cost_in_usd_cents: u64 = if self.is_ten_seconds { 95 } else { 48 }; + // 5s = 52¢, 10s = 103¢. + let cost_in_usd_cents: u64 = if self.is_ten_seconds { 103 } else { 52 }; VideoGenerationCostEstimate { cost_in_credits: Some(cost_in_usd_cents), @@ -51,11 +51,11 @@ mod tests { } #[test] - fn five_seconds_is_48() { assert_eq!(cost_cents(Some(5)), 48); } + fn five_seconds_is_52() { assert_eq!(cost_cents(Some(5)), 52); } #[test] - fn ten_seconds_is_95() { assert_eq!(cost_cents(Some(10)), 95); } + fn ten_seconds_is_103() { assert_eq!(cost_cents(Some(10)), 103); } #[test] - fn default_duration_is_5s_priced_at_48() { assert_eq!(cost_cents(None), 48); } + fn default_duration_is_5s_priced_at_52() { assert_eq!(cost_cents(None), 52); } } diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/build.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/build.rs index 1e91e10a76..7bf760aae4 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/build.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/build.rs @@ -20,7 +20,8 @@ pub fn build_artcraft_seedance_1p0_lite(mut builder: GenerateVideoRequestBuilder let request = build_artcraft_omni_video_request( builder, CommonVideoModelEnum::Seedance10Lite, - SupportedResolutions::Fast, + // The model serves 480p/720p/1080p; 1080p must plan (and price) as 1080p. + SupportedResolutions::Full, UltraWideSupport::Supported, )?; let state = ArtcraftSeedance10LiteRequestState { request }; diff --git a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/cost.rs b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/cost.rs index 015698590d..c6850d960f 100644 --- a/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/cost.rs +++ b/crates/api_clients/artcraft/artcraft_router/src/generate/generate_video/providers/artcraft/seedance_1p0_lite/cost.rs @@ -23,14 +23,15 @@ impl ArtcraftSeedance10LiteCostState { }; let resolution = match request.request.resolution { Some(CommonResolutionEnum::FourEightyP) => Seedance1LiteResolution::FourEightyP, + Some(CommonResolutionEnum::TenEightyP) => Seedance1LiteResolution::TenEightyP, _ => Seedance1LiteResolution::SevenTwentyP, }; Self { duration, resolution } } pub fn estimate_cost(&self) -> VideoGenerationCostEstimate { - // Delegate to the Fal client's cost calculator to guarantee parity with - // what we charge through the underlying provider. + // Derived from the Fal client's cost calculator so the price tracks + // upstream pricing across every resolution and duration. let req = Seedance1LiteRequest { image_url: String::new(), end_frame_image_url: None, @@ -41,7 +42,7 @@ impl ArtcraftSeedance10LiteCostState { camera_fixed: false, seed: None, }; - let cost_in_usd_cents = req.calculate_cost_in_cents(); + let cost_in_usd_cents = (req.calculate_cost_in_cents() * 21).div_ceil(20); VideoGenerationCostEstimate { cost_in_credits: Some(cost_in_usd_cents), @@ -75,22 +76,27 @@ mod tests { } #[test] - fn default_is_720p_5s_priced_at_18() { - // 720p + 5s is the special-cased price from the fal calculator: 18¢. - assert_eq!(cost_cents(None, None), 18); + fn default_is_720p_5s_priced_at_19() { + assert_eq!(cost_cents(None, None), 19); } #[test] - fn p720_5s_is_18() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::SevenTwentyP)), 18); } + fn p480_5s_is_10() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::FourEightyP)), 10); } #[test] - fn p720_10s_is_95() { - // 1280×720×30×10/1024 = 270000 tokens × $1.8/M = $0.486 → ceil = 49¢ ... - // No wait — fal kling not seedance. The Seedance 1.0 Lite rate is different. - // Trust the fal calculator: assert whatever the actual computed value is. - let v = cost_cents(Some(10), Some(RouterResolution::SevenTwentyP)); - assert!(v > 18, "10s should cost more than 5s, got {}", v); - } + fn p480_10s_is_18() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::FourEightyP)), 18); } + + #[test] + fn p720_5s_is_19() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::SevenTwentyP)), 19); } + + #[test] + fn p720_10s_is_52() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::SevenTwentyP)), 52); } + + #[test] + fn p1080_5s_is_58() { assert_eq!(cost_cents(Some(5), Some(RouterResolution::TenEightyP)), 58); } + + #[test] + fn p1080_10s_is_116() { assert_eq!(cost_cents(Some(10), Some(RouterResolution::TenEightyP)), 116); } #[test] fn p480_5s_cheaper_than_p720_5s() {