Compare commits

...
Author SHA1 Message Date
Brandon Thomas 41d209d46f fix models 2026-07-07 20:28:35 -04:00
4 changed files with 95 additions and 21 deletions
@@ -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<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
pub fn build_artcraft_kling_1_6_pro(mut builder: GenerateVideoRequestBuilder) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
// 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<u16>,
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<u16>, 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());
}
}
}
@@ -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); }
}
@@ -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 };
@@ -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() {