mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 17:08:13 +00:00
grok imagine video 1.5 preview (#1590)
This commit is contained in:
1 parent
5d4f307320
commit
0d7168c506
23 files changed
+2247
-19
No files matched your search
@@ -11,6 +11,9 @@ pub enum RouterVideoModel {
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
|
||||
+4
@@ -10,6 +10,7 @@ use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::build::build_artcraft_grok_imagine_video;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video_1p5::build::build_artcraft_grok_imagine_video_1p5;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::build::build_artcraft_happy_horse_1p0;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::kling_1_6_pro::build::build_artcraft_kling_1_6_pro;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::kling_2_1_master::build::build_artcraft_kling_2_1_master;
|
||||
@@ -42,6 +43,7 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::build::build_grok_api_grok_imagine_video;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video_1p5::build::build_grok_api_grok_imagine_video_1p5;
|
||||
use crate::generate::generate_video_v2::providers::fal::kling_1_6_pro::build::build_fal_kling_1_6_pro;
|
||||
use crate::generate::generate_video_v2::providers::fal::kling_2_1_master::build::build_fal_kling_2_1_master;
|
||||
use crate::generate::generate_video_v2::providers::fal::kling_2_1_pro::build::build_fal_kling_2_1_pro;
|
||||
@@ -151,6 +153,7 @@ impl GenerateVideoRequestBuilder {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(RouterProvider::Artcraft, RouterVideoModel::GrokImagineVideo) => build_artcraft_grok_imagine_video(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::GrokImagineVideo1p5) => build_artcraft_grok_imagine_video_1p5(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling16Pro) => build_artcraft_kling_1_6_pro(self),
|
||||
(RouterProvider::Artcraft, RouterVideoModel::Kling21Master) => build_artcraft_kling_2_1_master(self),
|
||||
@@ -200,6 +203,7 @@ impl GenerateVideoRequestBuilder {
|
||||
(RouterProvider::GmiCloud, RouterVideoModel::Seedance2p0UltraFast) => build_gmicloud_seedance_2p0_u_fast(self),
|
||||
// Grok
|
||||
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
|
||||
(RouterProvider::GrokApi, RouterVideoModel::GrokImagineVideo1p5) => build_grok_api_grok_imagine_video_1p5(self),
|
||||
// Kinovi
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(RouterProvider::Seedance2Pro, RouterVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video_1p5::request::ArtcraftGrokImagineVideo1p5RequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
/// Build an ArtCraft-routed Grok Imagine Video 1.5 preview request.
|
||||
///
|
||||
/// Same constraints as the v1 variant — xAI v1.5 still supports only 480p
|
||||
/// and 720p and no 21:9 ultra-wide. The on-wire model identifier is
|
||||
/// `grok-imagine-video-1.5-preview`; the cost calculator in
|
||||
/// [`super::cost`] keys off that to apply the v1.5 pricing tier (with a 5%
|
||||
/// ArtCraft markup on top of the upstream rates).
|
||||
pub fn build_artcraft_grok_imagine_video_1p5(
|
||||
builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_video_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::GrokImagineVideo1p5,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Unsupported,
|
||||
)?;
|
||||
let state = ArtcraftGrokImagineVideo1p5RequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo1p5(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_grok_imagine_video_1p5() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::GrokImagineVideo1p5)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
// SupportedResolutions::Fast caps at 720p (Grok Imagine doesn't render 1080p output).
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
// ArtCraft's omni endpoint requires media tokens — URLs are rejected
|
||||
// (unlike the direct Grok provider, which is URL-only).
|
||||
let result = build_artcraft_grok_imagine_video_1p5(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
provider: RouterProvider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftGrokImagineVideo1p5RequestState {
|
||||
let result = build_artcraft_grok_imagine_video_1p5(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo1p5(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftGrokImagineVideo1p5 request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+546
@@ -0,0 +1,546 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use grok_api_client::api::requests::videos::video_generation::video_generation::{
|
||||
VideoGenerationRequest as GrokVideoGenerationRequest,
|
||||
VideoImageSource as GrokVideoImageSource,
|
||||
};
|
||||
use grok_api_client::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator;
|
||||
use grok_api_client::api::types::video_types::video_model::VideoModel as GrokVideoModel;
|
||||
use grok_api_client::api::types::video_types::video_resolution::VideoResolution as GrokResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video_1p5::request::ArtcraftGrokImagineVideo1p5RequestState;
|
||||
|
||||
// ── Markup ──
|
||||
//
|
||||
// ArtCraft adds a modest 5% markup over the underlying xAI v1.5 pricing. The
|
||||
// markup is applied at cent granularity with ceiling rounding so the final
|
||||
// charge always covers the upstream cost (we never bill the user less than
|
||||
// the cost of the API call we forwarded).
|
||||
const MARKUP_NUMERATOR: u64 = 105;
|
||||
const MARKUP_DENOMINATOR: u64 = 100;
|
||||
|
||||
pub struct ArtcraftGrokImagineVideo1p5CostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub input_image_count: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftGrokImagineVideo1p5CostState {
|
||||
pub fn from_request(request: &ArtcraftGrokImagineVideo1p5RequestState) -> Self {
|
||||
let resolution = request.request.resolution.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(8);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
|
||||
// Mirror grok_api_client's input-image counting: start_frame + reference_images.
|
||||
let input_image_count = (request.request.start_frame_image_media_token.is_some() as u64)
|
||||
+ (request.request.reference_image_media_tokens
|
||||
.as_ref()
|
||||
.map(|v| v.len() as u64)
|
||||
.unwrap_or(0));
|
||||
|
||||
Self { resolution, duration_seconds, batch_count, input_image_count }
|
||||
}
|
||||
|
||||
/// Base cost in cents for a single video at this state's resolution,
|
||||
/// duration, and input-image count. Computed via grok_api_client's
|
||||
/// official cost calculator (with the model pinned to the v1.5 preview),
|
||||
/// so this number tracks upstream pricing automatically and never drifts
|
||||
/// from what xAI bills.
|
||||
///
|
||||
/// Does NOT include the per-batch multiplier and does NOT include the
|
||||
/// ArtCraft markup. Used by tests to assert parity with grok_api_client.
|
||||
pub fn base_cost_in_cents_for_one_video(&self) -> u64 {
|
||||
self.build_equivalent_grok_request().calculate_cost_in_cents()
|
||||
}
|
||||
|
||||
/// Total base cost (across the batch) in cents, before markup.
|
||||
/// Each video in the batch costs [`Self::base_cost_in_cents_for_one_video`].
|
||||
pub fn base_cost_in_cents_for_batch(&self) -> u64 {
|
||||
self.base_cost_in_cents_for_one_video()
|
||||
.saturating_mul(self.batch_count as u64)
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let base = self.base_cost_in_cents_for_batch();
|
||||
// 5% markup, ceil so the user is always charged enough to cover the
|
||||
// upstream grok cost regardless of rounding.
|
||||
let usd_cents = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a `GrokVideoGenerationRequest` whose pricing-relevant fields
|
||||
/// exactly mirror this cost state, so the grok_api_client calculator
|
||||
/// produces the same number ArtCraft would owe for one video. Non-pricing
|
||||
/// fields (prompt, aspect_ratio, user) are left empty — they don't affect
|
||||
/// cost.
|
||||
fn build_equivalent_grok_request(&self) -> GrokVideoGenerationRequest {
|
||||
let resolution = Some(match self.resolution {
|
||||
CommonResolution::FourEightyP => GrokResolution::FourEightyP,
|
||||
// Grok Imagine caps at 720p (and our build step downgrades higher
|
||||
// tiers via SupportedResolutions::Fast); price anything else as 720p.
|
||||
_ => GrokResolution::SevenTwentyP,
|
||||
});
|
||||
|
||||
// Grok's calculator only counts the TOTAL number of input images — it
|
||||
// doesn't care how they're split between `image` and `reference_images`
|
||||
// — so we put one in `image` (if any) and the rest in `reference_images`.
|
||||
let image = (self.input_image_count >= 1)
|
||||
.then(|| GrokVideoImageSource::Url(String::new()));
|
||||
let reference_images = if self.input_image_count >= 2 {
|
||||
Some(
|
||||
(1..self.input_image_count)
|
||||
.map(|_| GrokVideoImageSource::Url(String::new()))
|
||||
.collect()
|
||||
)
|
||||
} else { None };
|
||||
|
||||
GrokVideoGenerationRequest {
|
||||
prompt: String::new(),
|
||||
model: Some(GrokVideoModel::GrokImagineVideo1p5Preview),
|
||||
image,
|
||||
reference_images,
|
||||
aspect_ratio: None,
|
||||
duration: Some(self.duration_seconds as u32),
|
||||
resolution,
|
||||
user: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// ── Base cost parity with grok_api_client ─────────────────────────────
|
||||
//
|
||||
// The base cost (before ArtCraft markup, batch=1) must equal exactly what
|
||||
// grok_api_client's calculator returns for an equivalent direct-Grok
|
||||
// request. This protects against the artcraft and grok-api implementations
|
||||
// silently drifting apart.
|
||||
|
||||
mod grok_api_parity {
|
||||
use super::*;
|
||||
|
||||
// (resolution, duration, image_count)
|
||||
//
|
||||
// Durations are confined to [4, 15] because the ArtCraft pipeline clamps
|
||||
// outside that range (see `plan_duration` in artcraft/build_common.rs).
|
||||
const PARITY_CASES: &[(Option<RouterResolution>, u16, u64)] = &[
|
||||
// 480p variations
|
||||
(Some(RouterResolution::FourEightyP), 4, 0),
|
||||
(Some(RouterResolution::FourEightyP), 5, 0),
|
||||
(Some(RouterResolution::FourEightyP), 8, 0),
|
||||
(Some(RouterResolution::FourEightyP), 15, 0),
|
||||
(Some(RouterResolution::FourEightyP), 5, 1),
|
||||
(Some(RouterResolution::FourEightyP), 5, 3),
|
||||
// 720p variations
|
||||
(Some(RouterResolution::SevenTwentyP), 4, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 8, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 15, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 3),
|
||||
// None defaults to 720p in our `from_request`, which matches the
|
||||
// assumption Grok's calculator makes when fed a 720p request.
|
||||
(None, 5, 0),
|
||||
(None, 5, 2),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn base_cost_for_one_video_matches_grok_api_client() {
|
||||
for &(router_res, duration, image_count) in PARITY_CASES {
|
||||
let artcraft = artcraft_state(router_res, duration, image_count, 1);
|
||||
let grok_cents = grok_cents(router_res, duration, image_count);
|
||||
|
||||
assert_eq!(
|
||||
artcraft.base_cost_in_cents_for_one_video(),
|
||||
grok_cents,
|
||||
"parity mismatch at res={router_res:?} dur={duration} images={image_count}: \
|
||||
artcraft_base={} grok={}",
|
||||
artcraft.base_cost_in_cents_for_one_video(),
|
||||
grok_cents,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_one_base_total_equals_one_video_base() {
|
||||
let s = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, 1);
|
||||
assert_eq!(s.base_cost_in_cents_for_batch(), s.base_cost_in_cents_for_one_video());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_scales_base_linearly() {
|
||||
// The ArtCraft pipeline only accepts batch counts of 1, 2, or 4 (see
|
||||
// `plan_batch_count` in artcraft/build_common.rs); anything else gets
|
||||
// clamped, so we can only meaningfully test those values.
|
||||
let per_one = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, 1)
|
||||
.base_cost_in_cents_for_one_video();
|
||||
for batch in [1u16, 2, 4] {
|
||||
let s = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 1, batch);
|
||||
assert_eq!(
|
||||
s.base_cost_in_cents_for_batch(),
|
||||
per_one * batch as u64,
|
||||
"batch={batch}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Builder-driven parity — exercises the full pipeline (build2 →
|
||||
/// estimate_cost) on both providers and compares ArtCraft's base cost
|
||||
/// (final / 1.05, accounting for ceiling) against GrokApi's direct
|
||||
/// cost.
|
||||
///
|
||||
/// We verify the markup rather than the base here because the public
|
||||
/// `estimate_cost()` returns the marked-up number; the explicit
|
||||
/// `base_cost_in_cents_*` API is tested above.
|
||||
#[test]
|
||||
fn end_to_end_markup_is_at_least_five_percent_over_grok() {
|
||||
let cases: &[(Option<RouterResolution>, u16, bool, usize)] = &[
|
||||
(Some(RouterResolution::FourEightyP), 5, false, 0),
|
||||
(Some(RouterResolution::FourEightyP), 10, true, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, false, 0),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, false, 3),
|
||||
(Some(RouterResolution::SevenTwentyP), 8, true, 0),
|
||||
(Some(RouterResolution::SevenTwentyP),15, false, 0),
|
||||
];
|
||||
for &(res, duration, has_start, refs) in cases {
|
||||
let artcraft_cents = artcraft_end_to_end_cents(res, duration, 1, has_start, refs);
|
||||
let grok_cents = grok_cents(res, duration, has_start as u64 + refs as u64);
|
||||
let want_minimum = grok_cents.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
|
||||
assert_eq!(
|
||||
artcraft_cents, want_minimum,
|
||||
"res={res:?} dur={duration} start={has_start} refs={refs}: \
|
||||
artcraft={artcraft_cents}¢ vs ceil(grok({grok_cents})×1.05)={want_minimum}¢",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Markup correctness ─────────────────────────────────────────────────
|
||||
|
||||
mod markup {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn estimate_cost_equals_ceil_of_base_times_1p05() {
|
||||
for &(res, dur, imgs, batch) in MARKUP_CASES {
|
||||
let state = artcraft_state(res, dur, imgs, batch);
|
||||
let base = state.base_cost_in_cents_for_batch();
|
||||
let want = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
|
||||
let got = state.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
assert_eq!(
|
||||
got, want,
|
||||
"res={res:?} dur={dur} imgs={imgs} batch={batch} base={base}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_cost_is_always_at_least_base() {
|
||||
for &(res, dur, imgs, batch) in MARKUP_CASES {
|
||||
let state = artcraft_state(res, dur, imgs, batch);
|
||||
let base = state.base_cost_in_cents_for_batch();
|
||||
let final_ = state.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
assert!(
|
||||
final_ >= base,
|
||||
"final {final_}¢ < base {base}¢ at res={res:?} dur={dur} imgs={imgs} batch={batch}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_at_720p_5s_batch_1() {
|
||||
// grok v1.5 720p × 5s = 700 mills = 70¢
|
||||
// 70 × 1.05 = 73.5 → ceil = 74¢
|
||||
let state = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 1);
|
||||
assert_eq!(state.base_cost_in_cents_for_one_video(), 70);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents.unwrap(), 74);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_at_480p_5s_batch_1() {
|
||||
// grok v1.5 480p × 5s = 400 mills = 40¢
|
||||
// 40 × 1.05 = 42 → ceil = 42¢
|
||||
let state = artcraft_state(Some(RouterResolution::FourEightyP), 5, 0, 1);
|
||||
assert_eq!(state.base_cost_in_cents_for_one_video(), 40);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents.unwrap(), 42);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_value_at_720p_5s_batch_2() {
|
||||
// base for batch 2 = 70 × 2 = 140¢
|
||||
// 140 × 1.05 = 147 → ceil 147¢
|
||||
let state = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 2);
|
||||
assert_eq!(state.base_cost_in_cents_for_batch(), 140);
|
||||
assert_eq!(state.estimate_cost().cost_in_usd_cents.unwrap(), 147);
|
||||
}
|
||||
|
||||
// (resolution, duration, image_count, batch)
|
||||
const MARKUP_CASES: &[(Option<RouterResolution>, u16, u64, u16)] = &[
|
||||
(Some(RouterResolution::FourEightyP), 5, 0, 1),
|
||||
(Some(RouterResolution::FourEightyP), 5, 0, 2),
|
||||
(Some(RouterResolution::FourEightyP), 5, 0, 4),
|
||||
(Some(RouterResolution::FourEightyP), 8, 0, 1),
|
||||
(Some(RouterResolution::FourEightyP), 15, 0, 1),
|
||||
(Some(RouterResolution::FourEightyP), 5, 1, 1),
|
||||
(Some(RouterResolution::FourEightyP), 5, 3, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 0, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 0, 2),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 0, 4),
|
||||
(Some(RouterResolution::SevenTwentyP), 8, 0, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 15, 0, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 1, 1),
|
||||
(Some(RouterResolution::SevenTwentyP), 5, 3, 1),
|
||||
(None, 5, 0, 1),
|
||||
];
|
||||
}
|
||||
|
||||
// ── Relative pricing / monotonicity sanity ─────────────────────────────
|
||||
|
||||
mod monotonicity {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn higher_resolution_costs_more() {
|
||||
let p480 = artcraft_state(Some(RouterResolution::FourEightyP), 5, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
let p720 = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
assert!(p480 < p720, "480p ({p480}¢) should be < 720p ({p720}¢)");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longer_duration_costs_more() {
|
||||
let c5 = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
let c10 = artcraft_state(Some(RouterResolution::SevenTwentyP), 10, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
let c15 = artcraft_state(Some(RouterResolution::SevenTwentyP), 15, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
assert!(c5 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_images_cost_more_or_equal() {
|
||||
// Each image is +10 mills = +1¢ before markup, so 1+ image always
|
||||
// strictly increases final cost (the markup ceil pushes us up enough).
|
||||
let zero = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
let three = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 3, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
assert!(zero < three, "zero={zero}¢ vs three={three}¢");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn more_batch_costs_more() {
|
||||
let b1 = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 1)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
let b4 = artcraft_state(Some(RouterResolution::SevenTwentyP), 5, 0, 4)
|
||||
.estimate_cost().cost_in_usd_cents.unwrap();
|
||||
assert!(b1 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
// ── End-to-end through the public builder API ──────────────────────────
|
||||
|
||||
mod end_to_end {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents() {
|
||||
// ArtCraft credits == USD cents, as in the v1 module.
|
||||
let resolutions = [
|
||||
None,
|
||||
Some(RouterResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
Some(RouterResolution::TenEightyP), // clamps to 720p
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4u16, 5, 10, 15] {
|
||||
for batch in [1u16, 2, 4] {
|
||||
let cost = build_cost_via_builder(res, dur, batch);
|
||||
assert_eq!(
|
||||
cost.cost_in_credits, cost.cost_in_usd_cents,
|
||||
"credits should equal cents at res={res:?} dur={dur}s batch={batch}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_eighty_p_request_clamps_to_720p_pricing() {
|
||||
let p1080 = build_cost_via_builder(Some(RouterResolution::TenEightyP), 10, 1)
|
||||
.cost_in_usd_cents.unwrap();
|
||||
let p720 = build_cost_via_builder(Some(RouterResolution::SevenTwentyP), 10, 1)
|
||||
.cost_in_usd_cents.unwrap();
|
||||
assert_eq!(p1080, p720);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
fn artcraft_state(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
input_image_count: u64,
|
||||
batch_count: u16,
|
||||
) -> ArtcraftGrokImagineVideo1p5CostState {
|
||||
// Build a state directly from a synthesized request — avoids the full
|
||||
// build2() pipeline so we can construct arbitrary image counts without
|
||||
// worrying about builder validation rules.
|
||||
let state = unwrap_request_via_builder(resolution, duration_seconds, input_image_count, batch_count);
|
||||
ArtcraftGrokImagineVideo1p5CostState::from_request(&state)
|
||||
}
|
||||
|
||||
fn unwrap_request_via_builder(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
input_image_count: u64,
|
||||
batch_count: u16,
|
||||
) -> ArtcraftGrokImagineVideo1p5RequestState {
|
||||
// Split input_image_count into start_frame (1 if any) + reference_images (the rest),
|
||||
// mirroring how `from_request` counts source images.
|
||||
let start_frame = if input_image_count >= 1 {
|
||||
Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())))
|
||||
} else { None };
|
||||
let reference_images = if input_image_count >= 2 {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(1..input_image_count).map(|i| MediaFileToken::new(format!("mf_ref_{i}"))).collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(batch_count),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
let result = builder.build2().expect("build2 should succeed");
|
||||
match result {
|
||||
crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest::Request(
|
||||
crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest::ArtcraftGrokImagineVideo1p5(s)
|
||||
) => s,
|
||||
_ => panic!("expected ArtcraftGrokImagineVideo1p5 request"),
|
||||
}
|
||||
}
|
||||
|
||||
fn artcraft_end_to_end_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
batch_count: u16,
|
||||
has_start_frame: bool,
|
||||
num_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())))
|
||||
} else { None };
|
||||
let reference_images = if num_reference_images > 0 {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(0..num_reference_images)
|
||||
.map(|i| MediaFileToken::new(format!("mf_ref_{i}")))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(batch_count),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn build_cost_via_builder(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
/// Reference value: what GrokApi's own pipeline would charge for one
|
||||
/// equivalent video. Used by parity tests.
|
||||
///
|
||||
/// The Grok build step prefers `start_frame` over `reference_images` when
|
||||
/// both are supplied and drops the latter (see grok_api/grok_imagine_video_1p5/build.rs).
|
||||
/// To keep parity with the ArtCraft input-image *count*, we put all source
|
||||
/// images in `reference_images` only — the cost calculator counts the total
|
||||
/// either way.
|
||||
fn grok_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
image_count: u64,
|
||||
) -> u64 {
|
||||
let reference_images = if image_count >= 1 {
|
||||
Some(ImageListRef::Urls(
|
||||
(0..image_count).map(|i| format!("https://example.com/ref_{i}.png")).collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::GrokApi,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(1),
|
||||
start_frame: None,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 (grokapi) should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost (grokapi) should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_video_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGrokImagineVideo1p5RequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftGrokImagineVideo1p5RequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_video_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+1
@@ -2,6 +2,7 @@ pub(crate) mod build_common;
|
||||
#[cfg(test)]
|
||||
mod parity_tests;
|
||||
pub mod grok_imagine_video;
|
||||
pub mod grok_imagine_video_1p5;
|
||||
pub mod happy_horse_1p0;
|
||||
pub mod kling_1_6_pro;
|
||||
pub mod kling_2_1_master;
|
||||
|
||||
+650
@@ -0,0 +1,650 @@
|
||||
use log::info;
|
||||
|
||||
use grok_api_client::api::requests::videos::video_generation::video_generation::{
|
||||
VideoGenerationRequest as GrokVideoGenerationRequest,
|
||||
VideoImageSource as GrokVideoImageSource,
|
||||
};
|
||||
use grok_api_client::api::types::video_types::video_aspect_ratio::VideoAspectRatio as GrokAspectRatio;
|
||||
use grok_api_client::api::types::video_types::video_model::VideoModel as GrokVideoModel;
|
||||
use grok_api_client::api::types::video_types::video_resolution::VideoResolution as GrokResolution;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video_1p5::request::GrokApiGrokImagineVideo1p5RequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
/// Builds a Grok Imagine Video 1.5 preview request from the generic
|
||||
/// GenerateVideoRequestBuilder. Mirrors the v1 builder's capabilities and
|
||||
/// graceful-drop policy (the 1.5 model accepts the same wire shape as v1);
|
||||
/// the only material differences are the `model` identifier on the wire and
|
||||
/// the steeper pricing in the cost calculator (see `cost.rs`).
|
||||
///
|
||||
/// xAI's grok-imagine-video-1.5-preview accepts:
|
||||
/// - `image` (single source image, image-to-video mode) OR `reference_images`
|
||||
/// (multi-image reference-to-video). These two are mutually exclusive per
|
||||
/// xAI's API.
|
||||
/// - Aspect ratio, resolution (480p/720p), duration (1–15s), prompt.
|
||||
///
|
||||
/// Fields that Grok DOESN'T accept (`end_frame`, `reference_videos`,
|
||||
/// `reference_audio`, and `MediaFileToken`-style image refs) are silently
|
||||
/// dropped with an info-level log.
|
||||
pub fn build_grok_api_grok_imagine_video_1p5(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Scalar fields.
|
||||
let prompt = builder.prompt.take().unwrap_or_default();
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy);
|
||||
let resolution = plan_resolution(builder.resolution.take(), strategy);
|
||||
let duration = builder.duration_seconds.take().map(|d| (d as u32).clamp(1, 15));
|
||||
|
||||
log_and_drop_end_frame(builder.end_frame.take());
|
||||
log_and_drop_reference_videos(builder.reference_videos.take());
|
||||
log_and_drop_reference_audio(builder.reference_audio.take());
|
||||
|
||||
let start_frame = resolve_url_to_image_source(builder.start_frame.take());
|
||||
let reference_images_supplied = builder.reference_images.take();
|
||||
|
||||
// xAI's v1.5 model explicitly does NOT accept `reference_images` (it only
|
||||
// supports text-to-video and single-image-to-video). If the caller supplies
|
||||
// reference images and no start_frame, promote the first reference image
|
||||
// into `image` (image-to-video) and drop the rest.
|
||||
let (image, reference_images) = match (start_frame, reference_images_supplied) {
|
||||
(Some(img), Some(refs)) => {
|
||||
info!(
|
||||
"grok_imagine_video_1p5: both start_frame and reference_images supplied — \
|
||||
preferring start_frame (image-to-video). Dropping {} reference image(s).",
|
||||
count_image_list_ref(&refs),
|
||||
);
|
||||
(Some(img), None)
|
||||
}
|
||||
(Some(img), None) => (Some(img), None),
|
||||
(None, Some(refs)) => promote_first_reference_to_image(refs),
|
||||
(None, None) => (None, None),
|
||||
};
|
||||
|
||||
let request = GrokVideoGenerationRequest {
|
||||
prompt,
|
||||
// Pin to the 1.5 preview identifier so the grok_api_client cost
|
||||
// calculator picks the v1.5 pricing tier (see VideoModel::pricing_tier).
|
||||
model: Some(GrokVideoModel::GrokImagineVideo1p5Preview),
|
||||
image,
|
||||
reference_images,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
resolution,
|
||||
user: None,
|
||||
};
|
||||
|
||||
let state = GrokApiGrokImagineVideo1p5RequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo1p5(state),
|
||||
))
|
||||
}
|
||||
|
||||
// ── Field planners ──
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<RouterAspectRatio>,
|
||||
_strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Option<GrokAspectRatio> {
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(RouterAspectRatio::Auto)
|
||||
| Some(RouterAspectRatio::Auto2k)
|
||||
| Some(RouterAspectRatio::Auto3k)
|
||||
| Some(RouterAspectRatio::Auto4k) => None,
|
||||
|
||||
Some(RouterAspectRatio::Square) | Some(RouterAspectRatio::SquareHd) => {
|
||||
Some(GrokAspectRatio::Square)
|
||||
}
|
||||
|
||||
Some(RouterAspectRatio::WideSixteenByNine) | Some(RouterAspectRatio::Wide) => {
|
||||
Some(GrokAspectRatio::Landscape16x9)
|
||||
}
|
||||
Some(RouterAspectRatio::TallNineBySixteen) | Some(RouterAspectRatio::Tall) => {
|
||||
Some(GrokAspectRatio::Portrait9x16)
|
||||
}
|
||||
|
||||
Some(RouterAspectRatio::WideFourByThree) => Some(GrokAspectRatio::Landscape4x3),
|
||||
Some(RouterAspectRatio::TallThreeByFour) => Some(GrokAspectRatio::Portrait3x4),
|
||||
|
||||
Some(RouterAspectRatio::WideThreeByTwo) => Some(GrokAspectRatio::Landscape3x2),
|
||||
Some(RouterAspectRatio::TallTwoByThree) => Some(GrokAspectRatio::Portrait2x3),
|
||||
|
||||
Some(RouterAspectRatio::WideFiveByFour)
|
||||
| Some(RouterAspectRatio::WideTwentyOneByNine) => Some(GrokAspectRatio::Landscape16x9),
|
||||
Some(RouterAspectRatio::TallFourByFive)
|
||||
| Some(RouterAspectRatio::TallNineByTwentyOne) => Some(GrokAspectRatio::Portrait9x16),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<RouterResolution>,
|
||||
_strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Option<GrokResolution> {
|
||||
match resolution {
|
||||
None => None,
|
||||
Some(RouterResolution::FourEightyP) => Some(GrokResolution::FourEightyP),
|
||||
Some(RouterResolution::SevenTwentyP) => Some(GrokResolution::SevenTwentyP),
|
||||
Some(RouterResolution::TenEightyP)
|
||||
| Some(RouterResolution::TwoK)
|
||||
| Some(RouterResolution::ThreeK)
|
||||
| Some(RouterResolution::FourK) => Some(GrokResolution::SevenTwentyP),
|
||||
Some(RouterResolution::HalfK) | Some(RouterResolution::OneK) => {
|
||||
Some(GrokResolution::FourEightyP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image source resolvers (silent drops for unsupported variants) ──
|
||||
|
||||
fn resolve_url_to_image_source(image_ref: Option<ImageRef>) -> Option<GrokVideoImageSource> {
|
||||
match image_ref {
|
||||
None => None,
|
||||
Some(ImageRef::Url(url)) => Some(GrokVideoImageSource::Url(url)),
|
||||
Some(ImageRef::MediaFileToken(token)) => {
|
||||
info!(
|
||||
"grok_imagine_video_1p5: dropping MediaFileToken {:?} for start_frame — \
|
||||
Grok only accepts public URLs. Resolve the token to a URL upstream if needed.",
|
||||
token,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// xAI's v1.5 model rejects `reference_images` entirely. When a caller has
|
||||
/// only supplied reference images (no start_frame), promote the first one
|
||||
/// to `image` so they at least get image-to-video, and drop the remainder
|
||||
/// with a log so it's visible in operator traces.
|
||||
fn promote_first_reference_to_image(
|
||||
list_ref: ImageListRef,
|
||||
) -> (Option<GrokVideoImageSource>, Option<Vec<GrokVideoImageSource>>) {
|
||||
match list_ref {
|
||||
ImageListRef::Urls(urls) if urls.is_empty() => (None, None),
|
||||
ImageListRef::Urls(mut urls) => {
|
||||
let first_url = urls.remove(0);
|
||||
let dropped = urls.len();
|
||||
if dropped > 0 {
|
||||
info!(
|
||||
"grok_imagine_video_1p5: v1.5 doesn't accept reference_images; promoting \
|
||||
the first reference image to start_frame (image-to-video). Dropping {} \
|
||||
additional reference image(s).",
|
||||
dropped,
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
"grok_imagine_video_1p5: v1.5 doesn't accept reference_images; promoting \
|
||||
the single reference image to start_frame (image-to-video).",
|
||||
);
|
||||
}
|
||||
(Some(GrokVideoImageSource::Url(first_url)), None)
|
||||
}
|
||||
ImageListRef::MediaFileTokens(tokens) => {
|
||||
info!(
|
||||
"grok_imagine_video_1p5: dropping {} MediaFileToken reference image(s) — \
|
||||
Grok only accepts public URLs. Resolve tokens to URLs upstream if needed.",
|
||||
tokens.len(),
|
||||
);
|
||||
(None, None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_image_list_ref(refs: &ImageListRef) -> usize {
|
||||
match refs {
|
||||
ImageListRef::Urls(v) => v.len(),
|
||||
ImageListRef::MediaFileTokens(v) => v.len(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drop-with-log helpers for fields Grok doesn't accept at all ──
|
||||
|
||||
fn log_and_drop_end_frame(end_frame: Option<ImageRef>) {
|
||||
if end_frame.is_some() {
|
||||
info!("grok_imagine_video_1p5: dropping end_frame — Grok doesn't support end-frame keyframes.");
|
||||
}
|
||||
}
|
||||
|
||||
fn log_and_drop_reference_videos(refs: Option<VideoListRef>) {
|
||||
if let Some(refs) = refs {
|
||||
let count = match refs {
|
||||
VideoListRef::Urls(v) => v.len(),
|
||||
VideoListRef::MediaFileTokens(v) => v.len(),
|
||||
};
|
||||
info!(
|
||||
"grok_imagine_video_1p5: dropping {} reference video(s) — Grok doesn't accept reference videos.",
|
||||
count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_and_drop_reference_audio(refs: Option<AudioListRef>) {
|
||||
if let Some(refs) = refs {
|
||||
let count = match refs {
|
||||
AudioListRef::Urls(v) => v.len(),
|
||||
AudioListRef::MediaFileTokens(v) => v.len(),
|
||||
};
|
||||
info!(
|
||||
"grok_imagine_video_1p5: dropping {} reference audio clip(s) — Grok doesn't accept reference audio.",
|
||||
count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
// ── Wire-shape: the built request always carries the 1.5 preview model id ──
|
||||
|
||||
mod model_id {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn built_request_pins_grok_imagine_video_1p5_preview() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert_eq!(
|
||||
req.request.model,
|
||||
Some(GrokVideoModel::GrokImagineVideo1p5Preview),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn built_request_serializes_canonical_wire_name() {
|
||||
// The on-wire model string is the canonical name; the v1.5 cost
|
||||
// calculator in grok_api_client keys off this string.
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
let model = req.request.model.as_ref().expect("model should be set");
|
||||
assert_eq!(model.as_str(), "grok-imagine-video-1.5-preview");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Field passthrough ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test prompt".to_string()); }));
|
||||
assert_eq!(req.request.prompt, "test prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(8); }));
|
||||
assert_eq!(req.request.duration, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_min() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(0); }));
|
||||
assert_eq!(req.request.duration, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = None; }));
|
||||
assert_eq!(req.request.duration, None);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::Square); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_16x9() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_9x16() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallNineBySixteen); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait9x16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_4x3() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideFourByThree); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape4x3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_3x4() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallThreeByFour); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait3x4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_3x2() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::WideThreeByTwo); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape3x2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_2x3() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::TallTwoByThree); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait2x3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_maps_to_none() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(RouterAspectRatio::Auto); }));
|
||||
assert_eq!(req.request.aspect_ratio, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_wide_falls_back_to_16x9() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.aspect_ratio = Some(RouterAspectRatio::WideTwentyOneByNine);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_tall_falls_back_to_9x16() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.aspect_ratio = Some(RouterAspectRatio::TallNineByTwentyOne);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait9x16));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_clamps_to_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_4k_clamps_to_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::FourK); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1k_bumps_to_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(RouterResolution::OneK); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image source plumbing (graceful — never errors) ──
|
||||
|
||||
mod image_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_url_becomes_image() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
}));
|
||||
match &req.request.image {
|
||||
Some(GrokVideoImageSource::Url(u)) => assert_eq!(u, "https://example.com/start.png"),
|
||||
other => panic!("expected Url variant, got {:?}", other),
|
||||
}
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_media_file_token_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test".to_string())));
|
||||
}));
|
||||
assert!(req.request.image.is_none());
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_reference_image_urls_promote_first_and_drop_rest() {
|
||||
// xAI's v1.5 model rejects `reference_images`. The builder promotes the
|
||||
// first reference to `image` and drops the remainder.
|
||||
let urls = vec![
|
||||
"https://example.com/a.png".to_string(),
|
||||
"https://example.com/b.png".to_string(),
|
||||
"https://example.com/c.png".to_string(),
|
||||
];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::Urls(urls.clone()));
|
||||
}));
|
||||
assert!(
|
||||
req.request.reference_images.is_none(),
|
||||
"reference_images must be None (xAI v1.5 rejects them)",
|
||||
);
|
||||
match &req.request.image {
|
||||
Some(GrokVideoImageSource::Url(u)) => assert_eq!(u, "https://example.com/a.png"),
|
||||
other => panic!("expected first reference image promoted to `image`, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_reference_image_url_promotes_to_image() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/only.png".to_string(),
|
||||
]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
match &req.request.image {
|
||||
Some(GrokVideoImageSource::Url(u)) => assert_eq!(u, "https://example.com/only.png"),
|
||||
other => panic!("expected single reference promoted to `image`, got {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Replicates the production failure from 2026-05-31: storyteller-web
|
||||
/// received `reference_image_media_tokens: [..., ...]` and routed it to
|
||||
/// the v1.5 model, which rejected the request with
|
||||
/// `reference_images is not supported for this model`. After the
|
||||
/// promote-first fix, the resolved URLs go into `image` instead.
|
||||
#[test]
|
||||
fn reproduces_2026_05_31_two_ref_image_urls_no_start_frame() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
// Two URLs (the storyteller-web pipeline resolves media tokens to URLs
|
||||
// before calling build2() on the GrokApi provider).
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![
|
||||
"https://pub.example.com/media/a.png".to_string(),
|
||||
"https://pub.example.com/media/b.png".to_string(),
|
||||
]));
|
||||
b.start_frame = None;
|
||||
b.resolution = Some(RouterResolution::SevenTwentyP);
|
||||
b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine);
|
||||
b.duration_seconds = Some(5);
|
||||
}));
|
||||
// The fix: no `reference_images` ever reach xAI for v1.5.
|
||||
assert!(
|
||||
req.request.reference_images.is_none(),
|
||||
"v1.5 must never send reference_images to xAI",
|
||||
);
|
||||
// The first reference is preserved as image-to-video so the user still
|
||||
// gets a sensible result.
|
||||
assert!(matches!(req.request.image, Some(GrokVideoImageSource::Url(_))));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
assert_eq!(req.request.duration, Some(5));
|
||||
assert_eq!(req.request.model, Some(GrokVideoModel::GrokImagineVideo1p5Preview));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
assert!(req.request.image.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_and_reference_images_together_prefers_start_frame() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/ref1.png".to_string(),
|
||||
"https://example.com/ref2.png".to_string(),
|
||||
]));
|
||||
}));
|
||||
match &req.request.image {
|
||||
Some(GrokVideoImageSource::Url(u)) => assert_eq!(u, "https://example.com/start.png"),
|
||||
other => panic!("expected start_frame URL, got {:?}", other),
|
||||
}
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_reference_image_url_list_becomes_none() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unsupported fields are silently dropped, not rejected ──
|
||||
|
||||
mod unsupported_fields_graceful {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn end_frame_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string()));
|
||||
}));
|
||||
assert!(req.request.image.is_none());
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_videos_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_videos = Some(VideoListRef::Urls(vec!["https://example.com/v.mp4".to_string()]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_audio_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_audio = Some(AudioListRef::Urls(vec!["https://example.com/a.wav".to_string()]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitchen_sink_with_every_unsupported_field_still_succeeds() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.prompt = Some("kitchen sink".to_string());
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string()));
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/ref.png".to_string(),
|
||||
]));
|
||||
b.reference_videos = Some(VideoListRef::Urls(vec!["https://example.com/v.mp4".to_string()]));
|
||||
b.reference_audio = Some(AudioListRef::Urls(vec!["https://example.com/a.wav".to_string()]));
|
||||
b.resolution = Some(RouterResolution::SevenTwentyP);
|
||||
b.aspect_ratio = Some(RouterAspectRatio::WideSixteenByNine);
|
||||
b.duration_seconds = Some(8);
|
||||
}));
|
||||
assert_eq!(req.request.prompt, "kitchen sink");
|
||||
assert!(matches!(
|
||||
req.request.image,
|
||||
Some(GrokVideoImageSource::Url(_))
|
||||
));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
assert_eq!(req.request.duration, Some(8));
|
||||
// Still pinned to the 1.5 model regardless of kitchen-sink inputs.
|
||||
assert_eq!(req.request.model, Some(GrokVideoModel::GrokImagineVideo1p5Preview));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::GrokApi,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut b = base_builder();
|
||||
f(&mut b);
|
||||
b
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GrokApiGrokImagineVideo1p5RequestState {
|
||||
let result = build_grok_api_grok_imagine_video_1p5(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GrokApiGrokImagineVideo1p5(s)) => s,
|
||||
_ => panic!("expected GrokApiGrokImagineVideo1p5 request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+308
@@ -0,0 +1,308 @@
|
||||
use grok_api_client::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video_1p5::request::GrokApiGrokImagineVideo1p5RequestState;
|
||||
|
||||
pub struct GrokApiGrokImagineVideo1p5CostState {
|
||||
request: GrokApiGrokImagineVideo1p5RequestState,
|
||||
}
|
||||
|
||||
impl GrokApiGrokImagineVideo1p5CostState {
|
||||
pub fn from_request(request: &GrokApiGrokImagineVideo1p5RequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// grok_api_client's calculator picks the v1.5 pricing tier from the
|
||||
// request's `model` field (see VideoModel::pricing_tier). The build step
|
||||
// sets `model = Some(GrokImagineVideo1p5Preview)`, so this returns the
|
||||
// v1.5 rates:
|
||||
// Output: 80 mills/s @ 480p, 140 mills/s @ 720p
|
||||
// Input: 10 mills per source image
|
||||
let cost_in_usd_cents = self.request.request.calculate_cost_in_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
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 crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// v1.5 pricing reference (from grok_api_client/video_generation/cost.rs):
|
||||
// Output: 480p 80 mills/s ($0.08/s), 720p 140 mills/s ($0.14/s)
|
||||
// Input: 10 mills per source image ($0.01/img)
|
||||
//
|
||||
// Cost in cents = ceil(mills / 10).
|
||||
|
||||
// ── 720p pricing ──
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn five_s_no_image_ref() {
|
||||
// 140 × 5 = 700 mills → 70¢
|
||||
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 5, None, None), 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_s_no_image_ref() {
|
||||
// 140 × 10 = 1400 mills → 140¢
|
||||
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 10, None, None), 140);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifteen_s_no_image_ref() {
|
||||
// 140 × 15 = 2100 mills → 210¢
|
||||
assert_eq!(cost_cents(Some(RouterResolution::SevenTwentyP), 15, None, None), 210);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 480p pricing ──
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn five_s_no_image_ref() {
|
||||
// 80 × 5 = 400 mills → 40¢
|
||||
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 5, None, None), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_s_no_image_ref() {
|
||||
// 80 × 10 = 800 mills → 80¢
|
||||
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 10, None, None), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifteen_s_no_image_ref() {
|
||||
// 80 × 15 = 1200 mills → 120¢
|
||||
assert_eq!(cost_cents(Some(RouterResolution::FourEightyP), 15, None, None), 120);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image input pricing (10 mills/image, NOT 2 mills as in v1) ──
|
||||
|
||||
mod image_inputs_add_to_cost {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_start_frame_image_adds_10_mills() {
|
||||
// 5s @ 720p output 700 mills + 1 input image 10 mills = 710 mills → 71¢
|
||||
let cents = cost_cents(
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
5,
|
||||
Some(ImageRef::Url("https://example.com/a.png".to_string())),
|
||||
None,
|
||||
);
|
||||
assert_eq!(cents, 71);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_reference_images_adds_30_mills() {
|
||||
// 5s @ 720p output 700 mills + 3 input images 30 mills = 730 mills → 73¢
|
||||
let cents = cost_cents(
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
5,
|
||||
None,
|
||||
Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/a.png".to_string(),
|
||||
"https://example.com/b.png".to_string(),
|
||||
"https://example.com/c.png".to_string(),
|
||||
])),
|
||||
);
|
||||
assert_eq!(cents, 73);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn image_input_count_scales_linearly() {
|
||||
// Fix 5s @ 720p (700 mills base); vary ref image count 1..=5.
|
||||
for n in 1u64..=5 {
|
||||
let images: Vec<String> = (0..n)
|
||||
.map(|i| format!("https://example.com/{i}.png"))
|
||||
.collect();
|
||||
let cents = cost_cents(
|
||||
Some(RouterResolution::SevenTwentyP),
|
||||
5,
|
||||
None,
|
||||
Some(ImageListRef::Urls(images)),
|
||||
);
|
||||
let expected_mills = 700 + 10 * n;
|
||||
let expected_cents = expected_mills.div_ceil(10);
|
||||
assert_eq!(cents, expected_cents, "n={n}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relative pricing sanity ──
|
||||
|
||||
mod relative_pricing {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn higher_resolution_costs_more() {
|
||||
let p480 = cost_cents(Some(RouterResolution::FourEightyP), 10, None, None);
|
||||
let p720 = cost_cents(Some(RouterResolution::SevenTwentyP), 10, None, None);
|
||||
assert!(p480 < p720, "480p ({p480}) should be < 720p ({p720})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longer_duration_costs_more() {
|
||||
let c5 = cost_cents(Some(RouterResolution::SevenTwentyP), 5, None, None);
|
||||
let c10 = cost_cents(Some(RouterResolution::SevenTwentyP), 10, None, None);
|
||||
let c15 = cost_cents(Some(RouterResolution::SevenTwentyP), 15, None, None);
|
||||
assert!(c5 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_request_clamps_to_480p_pricing() {
|
||||
let one_k = cost_cents(Some(RouterResolution::OneK), 10, None, None);
|
||||
let p480 = cost_cents(Some(RouterResolution::FourEightyP), 10, None, None);
|
||||
assert_eq!(one_k, p480);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_eighty_p_request_clamps_to_720p_pricing() {
|
||||
let p1080 = cost_cents(Some(RouterResolution::TenEightyP), 10, None, None);
|
||||
let p720 = cost_cents(Some(RouterResolution::SevenTwentyP), 10, None, None);
|
||||
assert_eq!(p1080, p720);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-tier sanity: 1.5 must cost more than v1 at same settings ──
|
||||
|
||||
mod cross_tier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn v1p5_costs_more_than_v1_at_720p() {
|
||||
let v1p5 = cost_cents(Some(RouterResolution::SevenTwentyP), 5, None, None);
|
||||
let v1 = v1_cost_cents(Some(RouterResolution::SevenTwentyP), 5, None, None);
|
||||
assert!(v1 < v1p5, "v1={v1} v1p5={v1p5}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1p5_costs_more_than_v1_at_480p() {
|
||||
let v1p5 = cost_cents(Some(RouterResolution::FourEightyP), 5, None, None);
|
||||
let v1 = v1_cost_cents(Some(RouterResolution::FourEightyP), 5, None, None);
|
||||
assert!(v1 < v1p5, "v1={v1} v1p5={v1p5}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Exhaustive matrix ──
|
||||
|
||||
mod exhaustive_matrix {
|
||||
use super::*;
|
||||
|
||||
// (resolution, duration_s, has_image, ref_count, expected_cents)
|
||||
//
|
||||
// Mills math:
|
||||
// 480p output: 80 mills/s 720p output: 140 mills/s
|
||||
// per input image: 10 mills
|
||||
// cents = ceil(mills / 10)
|
||||
const CASES: &[(RouterResolution, u16, bool, usize, u64)] = &[
|
||||
// 480p text-only
|
||||
(RouterResolution::FourEightyP, 1, false, 0, 8), // 80
|
||||
(RouterResolution::FourEightyP, 5, false, 0, 40), // 400
|
||||
(RouterResolution::FourEightyP, 8, false, 0, 64), // 640
|
||||
(RouterResolution::FourEightyP, 15, false, 0, 120), // 1200
|
||||
// 720p text-only
|
||||
(RouterResolution::SevenTwentyP, 1, false, 0, 14), // 140
|
||||
(RouterResolution::SevenTwentyP, 5, false, 0, 70), // 700
|
||||
(RouterResolution::SevenTwentyP, 8, false, 0, 112), // 1120
|
||||
(RouterResolution::SevenTwentyP,15, false, 0, 210), // 2100
|
||||
// 480p with image-to-video (1 input image, +10 mills)
|
||||
(RouterResolution::FourEightyP, 5, true, 0, 41), // 410
|
||||
(RouterResolution::FourEightyP, 10, true, 0, 81), // 810
|
||||
// 720p with image-to-video (1 input image, +10 mills)
|
||||
(RouterResolution::SevenTwentyP, 5, true, 0, 71), // 710
|
||||
(RouterResolution::SevenTwentyP,10, true, 0, 141), // 1410
|
||||
// 720p reference-to-video (no start frame, N reference images)
|
||||
(RouterResolution::SevenTwentyP, 5, false, 1, 71), // 710
|
||||
(RouterResolution::SevenTwentyP, 5, false, 2, 72), // 720
|
||||
(RouterResolution::SevenTwentyP, 5, false, 3, 73), // 730
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn all_matrix_cases() {
|
||||
for &(res, duration, has_image, ref_count, expected_cents) in CASES {
|
||||
let start_frame = if has_image {
|
||||
Some(ImageRef::Url("https://example.com/start.png".to_string()))
|
||||
} else { None };
|
||||
let reference_images = if ref_count > 0 {
|
||||
Some(ImageListRef::Urls(
|
||||
(0..ref_count).map(|i| format!("https://example.com/ref_{i}.png")).collect(),
|
||||
))
|
||||
} else { None };
|
||||
let cents = cost_cents(Some(res), duration, start_frame, reference_images);
|
||||
assert_eq!(
|
||||
cents, expected_cents,
|
||||
"res={res:?} dur={duration} has_image={has_image} ref_count={ref_count}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn cost_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
start_frame: Option<ImageRef>,
|
||||
reference_images: Option<ImageListRef>,
|
||||
) -> u64 {
|
||||
build_for(RouterVideoModel::GrokImagineVideo1p5, resolution, duration_seconds, start_frame, reference_images)
|
||||
}
|
||||
|
||||
fn v1_cost_cents(
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
start_frame: Option<ImageRef>,
|
||||
reference_images: Option<ImageListRef>,
|
||||
) -> u64 {
|
||||
build_for(RouterVideoModel::GrokImagineVideo, resolution, duration_seconds, start_frame, reference_images)
|
||||
}
|
||||
|
||||
fn build_for(
|
||||
model: RouterVideoModel,
|
||||
resolution: Option<RouterResolution>,
|
||||
duration_seconds: u16,
|
||||
start_frame: Option<ImageRef>,
|
||||
reference_images: Option<ImageListRef>,
|
||||
) -> u64 {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model,
|
||||
provider: RouterProvider::GrokApi,
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(1),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use grok_api_client::api::requests::videos::video_generation::video_generation::{
|
||||
video_generation, VideoGenerationArgs, VideoGenerationRequest as GrokVideoGenerationRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
GenerateVideoResponse, GrokVideoResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GrokApiGrokImagineVideo1p5RequestState {
|
||||
/// The fully-resolved Grok request body, with `model` set to
|
||||
/// `VideoModel::GrokImagineVideo1p5Preview`. Doesn't carry the API key —
|
||||
/// that gets borrowed at send time from the router client.
|
||||
pub request: GrokVideoGenerationRequest,
|
||||
}
|
||||
|
||||
impl GrokApiGrokImagineVideo1p5RequestState {
|
||||
pub async fn send(&self, client: &RouterGrokApiClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let outbound_request = Arc::new(self.request.clone());
|
||||
|
||||
let response = video_generation(VideoGenerationArgs {
|
||||
api_key: &client.api_key,
|
||||
request: self.request.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Grok(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Grok(GrokVideoResponsePayload {
|
||||
request_id: response.request_id,
|
||||
maybe_outbound_request: Some(outbound_request),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use grok_api_client::api::types::video_types::video_model::VideoModel;
|
||||
use grok_api_client::creds::grok_api_key::GrokApiKey;
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
use crate::api::router_aspect_ratio::RouterAspectRatio;
|
||||
use crate::api::router_resolution::RouterResolution;
|
||||
use crate::api::router_video_model::RouterVideoModel;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::router_provider::RouterProvider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
// ── Wire-shape sanity: the built request always carries the 1.5 model id ──
|
||||
|
||||
#[test]
|
||||
fn built_request_targets_grok_imagine_video_1p5_preview() {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
prompt: Some("test".to_string()),
|
||||
resolution: Some(RouterResolution::SevenTwentyP),
|
||||
..grok_builder()
|
||||
};
|
||||
let request = unwrap_request(builder);
|
||||
assert_eq!(request.request.model, Some(VideoModel::GrokImagineVideo1p5Preview));
|
||||
}
|
||||
|
||||
// ── Live API tests (manual) ──
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs costs
|
||||
async fn test_text_to_video_720p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A glowing crystal rocket launching from Mars.".to_string()),
|
||||
aspect_ratio: Some(RouterAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(RouterResolution::SevenTwentyP),
|
||||
duration_seconds: Some(5),
|
||||
..grok_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Grok(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs costs
|
||||
async fn test_image_to_video_480p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("The dog leaps into the lake and splashes around.".to_string()),
|
||||
start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())),
|
||||
resolution: Some(RouterResolution::FourEightyP),
|
||||
duration_seconds: Some(5),
|
||||
..grok_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Grok(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs costs
|
||||
async fn test_image_to_video_720p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("The dog leaps into the lake and splashes around.".to_string()),
|
||||
start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())),
|
||||
resolution: Some(RouterResolution::SevenTwentyP),
|
||||
duration_seconds: Some(5),
|
||||
..grok_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Grok(_)));
|
||||
}
|
||||
|
||||
fn grok_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: RouterVideoModel::GrokImagineVideo1p5,
|
||||
provider: RouterProvider::GrokApi,
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_grok_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/grok_api_key.txt")
|
||||
.expect("Failed to read Grok API key");
|
||||
let api_key = GrokApiKey::new(secret.trim().to_string());
|
||||
RouterClient::GrokApi(RouterGrokApiClient::new(api_key))
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GrokApiGrokImagineVideo1p5RequestState {
|
||||
let result = builder.build2().expect("build2 should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest::GrokApiGrokImagineVideo1p5(s)
|
||||
) => s,
|
||||
_ => panic!("expected GrokApiGrokImagineVideo1p5 request"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_grok_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Grok skips draft)"),
|
||||
};
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
match &response {
|
||||
GenerateVideoResponse::Grok(p) => println!("grok request_id={}", p.request_id),
|
||||
other => println!("unexpected response: {:?}", other),
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
+1
@@ -1 +1,2 @@
|
||||
pub mod grok_imagine_video;
|
||||
pub mod grok_imagine_video_1p5;
|
||||
+18
@@ -5,6 +5,8 @@ use crate::generate::generate_video::generate_video_response::GenerateVideoRespo
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::cost::ArtcraftGrokImagineVideoCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::request::ArtcraftGrokImagineVideoRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video_1p5::cost::ArtcraftGrokImagineVideo1p5CostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video_1p5::request::ArtcraftGrokImagineVideo1p5RequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::cost::ArtcraftHappyHorse1p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::kling_1_6_pro::cost::ArtcraftKling16ProCostState;
|
||||
@@ -97,6 +99,8 @@ use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::cost::GrokApiGrokImagineVideoCostState;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::request::GrokApiGrokImagineVideoRequestState;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video_1p5::cost::GrokApiGrokImagineVideo1p5CostState;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video_1p5::request::GrokApiGrokImagineVideo1p5RequestState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::cost::KinoviHappyHorse1p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::request::KinoviHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState;
|
||||
@@ -107,6 +111,7 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::re
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoGenerationRequest {
|
||||
ArtcraftGrokImagineVideo(ArtcraftGrokImagineVideoRequestState),
|
||||
ArtcraftGrokImagineVideo1p5(ArtcraftGrokImagineVideo1p5RequestState),
|
||||
ArtcraftHappyHorse1p0(ArtcraftHappyHorse1p0RequestState),
|
||||
ArtcraftKling16Pro(ArtcraftKling16ProRequestState),
|
||||
ArtcraftKling21Master(ArtcraftKling21MasterRequestState),
|
||||
@@ -153,6 +158,7 @@ pub enum VideoGenerationRequest {
|
||||
GmiCloudSeedance2p0Ultra(GmiCloudSeedance2p0UltraRequestState),
|
||||
GmiCloudSeedance2p0UltraFast(GmiCloudSeedance2p0UltraFastRequestState),
|
||||
GrokApiGrokImagineVideo(GrokApiGrokImagineVideoRequestState),
|
||||
GrokApiGrokImagineVideo1p5(GrokApiGrokImagineVideo1p5RequestState),
|
||||
KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState),
|
||||
KinoviSeedance2p0(KinoviSeedance2p0RequestState),
|
||||
KinoviSeedance2p0Fast(KinoviSeedance2p0FastRequestState),
|
||||
@@ -163,6 +169,7 @@ impl VideoGenerationRequest {
|
||||
pub fn get_provider(&self) -> RouterProvider {
|
||||
match self {
|
||||
Self::ArtcraftGrokImagineVideo(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftGrokImagineVideo1p5(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftHappyHorse1p0(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftKling16Pro(_) => RouterProvider::Artcraft,
|
||||
Self::ArtcraftKling21Master(_) => RouterProvider::Artcraft,
|
||||
@@ -209,6 +216,7 @@ impl VideoGenerationRequest {
|
||||
Self::GmiCloudSeedance2p0Ultra(_) => RouterProvider::GmiCloud,
|
||||
Self::GmiCloudSeedance2p0UltraFast(_) => RouterProvider::GmiCloud,
|
||||
Self::GrokApiGrokImagineVideo(_) => RouterProvider::GrokApi,
|
||||
Self::GrokApiGrokImagineVideo1p5(_) => RouterProvider::GrokApi,
|
||||
Self::KinoviHappyHorse1p0(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0(_) => RouterProvider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Fast(_) => RouterProvider::Seedance2Pro,
|
||||
@@ -219,6 +227,7 @@ impl VideoGenerationRequest {
|
||||
pub fn estimate_cost(&self) -> Result<VideoGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo(request) => Ok(ArtcraftGrokImagineVideoCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo1p5(request) => Ok(ArtcraftGrokImagineVideo1p5CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => Ok(ArtcraftHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftKling16Pro(request) => Ok(ArtcraftKling16ProCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftKling21Master(request) => Ok(ArtcraftKling21MasterCostState::from_request(request).estimate_cost()),
|
||||
@@ -265,6 +274,7 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => Ok(GmiCloudSeedance2p0UltraCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => Ok(GmiCloudSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo(request) => Ok(GrokApiGrokImagineVideoCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo1p5(request) => Ok(GrokApiGrokImagineVideo1p5CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0(request) => Ok(KinoviSeedance2p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => Ok(KinoviSeedance2p0FastCostState::from_request(request).estimate_cost()),
|
||||
@@ -279,6 +289,10 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo1p5(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
@@ -463,6 +477,10 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_grok_api_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo1p5(request) => {
|
||||
let client_ref = client.get_grok_api_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::KinoviHappyHorse1p0(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
|
||||
+178
-14
@@ -1,27 +1,39 @@
|
||||
use crate::api::requests::videos::video_generation::video_generation::VideoGenerationRequest;
|
||||
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
|
||||
use crate::api::types::video_types::video_model::{VideoModel, VideoModelPricingTier};
|
||||
use crate::api::types::video_types::video_resolution::VideoResolution;
|
||||
|
||||
// xAI pricing per <https://docs.x.ai/developers/pricing>:
|
||||
// xAI pricing per <https://docs.x.ai/developers/pricing> and
|
||||
// <https://docs.x.ai/developers/models/grok-imagine-video-1.5-preview>:
|
||||
//
|
||||
// grok-imagine-video
|
||||
// grok-imagine-video (v1)
|
||||
// Output: 480p $0.05/sec, 720p $0.07/sec
|
||||
// Input: $0.01/sec (source video) + $0.002/img (source images)
|
||||
//
|
||||
// grok-imagine-video-1.5-preview (v1.5)
|
||||
// Output: 480p $0.08/sec, 720p $0.14/sec
|
||||
// Input: $0.01/img (source images)
|
||||
//
|
||||
// In mills (1¢ = 10 mills):
|
||||
//
|
||||
// Output: 480p 50 mills/sec, 720p 70 mills/sec
|
||||
// Input: 10 mills/sec (video), 2 mills/img (per source image)
|
||||
// v1 output: 480p 50 mills/sec, 720p 70 mills/sec
|
||||
// v1 input: 10 mills/sec (video), 2 mills/img
|
||||
// v1.5 output: 480p 80 mills/sec, 720p 140 mills/sec
|
||||
// v1.5 input: 10 mills/img
|
||||
//
|
||||
// video_generation has NO input video — only optional input images. xAI
|
||||
// defaults: `duration` = 8 seconds, `resolution` = "480p".
|
||||
// defaults: `duration` = 8 seconds, `resolution` = "480p" (the server side;
|
||||
// our local default below is 720p — see DEFAULT comments in the impl).
|
||||
|
||||
/// xAI default duration for video_generation when `duration` is omitted.
|
||||
pub(crate) const DEFAULT_VIDEO_DURATION_SECONDS: u32 = 8;
|
||||
|
||||
/// Per-source-image input rate for video endpoints, in mills.
|
||||
/// Per-source-image input rate (v1 tier), in mills.
|
||||
pub(crate) const INPUT_MILLS_PER_IMAGE: UsdMills = 2;
|
||||
|
||||
/// Per-source-image input rate (v1.5 tier), in mills.
|
||||
const INPUT_MILLS_PER_IMAGE_V1P5: UsdMills = 10;
|
||||
|
||||
/// Per-source-second input rate (when a source video is supplied) for video
|
||||
/// endpoints, in mills. Not used by `video_generation` (which has no source
|
||||
/// video) but referenced by `video_edit` and `video_extension`.
|
||||
@@ -31,8 +43,12 @@ impl GrokRequestCostCalculator for VideoGenerationRequest {
|
||||
fn calculate_cost_in_mills(&self) -> UsdMills {
|
||||
let duration = self.duration.unwrap_or(DEFAULT_VIDEO_DURATION_SECONDS) as u64;
|
||||
let resolution = self.resolution.unwrap_or(VideoResolution::SevenTwentyP);
|
||||
let tier = self.model
|
||||
.as_ref()
|
||||
.map(VideoModel::pricing_tier)
|
||||
.unwrap_or(VideoModelPricingTier::V1);
|
||||
|
||||
let output_mills = output_mills_per_second(resolution) * duration;
|
||||
let output_mills = output_mills_per_second_for_tier(resolution, tier) * duration;
|
||||
|
||||
// Source image count = `image` (image-to-video) + len(`reference_images`)
|
||||
// (reference-to-video). These are mutually exclusive at call time but the
|
||||
@@ -41,17 +57,38 @@ impl GrokRequestCostCalculator for VideoGenerationRequest {
|
||||
(self.image.is_some() as u64)
|
||||
+ (self.reference_images.as_ref().map(|v| v.len() as u64).unwrap_or(0));
|
||||
|
||||
let input_mills = INPUT_MILLS_PER_IMAGE * source_image_count;
|
||||
let input_mills = input_mills_per_image(tier) * source_image_count;
|
||||
|
||||
output_mills + input_mills
|
||||
}
|
||||
}
|
||||
|
||||
/// Output rate in mills per second of generated video, by resolution.
|
||||
/// Output rate in mills per second of generated video at v1 rates, by
|
||||
/// resolution. Kept for cross-module use by `video_edit` and
|
||||
/// `video_extension`, which both assume v1 pricing and do not currently take
|
||||
/// a model into account in their cost calculators.
|
||||
pub(crate) fn output_mills_per_second(resolution: VideoResolution) -> UsdMills {
|
||||
match resolution {
|
||||
VideoResolution::FourEightyP => 50,
|
||||
VideoResolution::SevenTwentyP => 70,
|
||||
output_mills_per_second_for_tier(resolution, VideoModelPricingTier::V1)
|
||||
}
|
||||
|
||||
/// Output rate in mills per second of generated video, by resolution AND
|
||||
/// pricing tier. Used by `video_generation`.
|
||||
fn output_mills_per_second_for_tier(
|
||||
resolution: VideoResolution,
|
||||
tier: VideoModelPricingTier,
|
||||
) -> UsdMills {
|
||||
match (tier, resolution) {
|
||||
(VideoModelPricingTier::V1, VideoResolution::FourEightyP) => 50,
|
||||
(VideoModelPricingTier::V1, VideoResolution::SevenTwentyP) => 70,
|
||||
(VideoModelPricingTier::V1p5Preview, VideoResolution::FourEightyP) => 80,
|
||||
(VideoModelPricingTier::V1p5Preview, VideoResolution::SevenTwentyP) => 140,
|
||||
}
|
||||
}
|
||||
|
||||
fn input_mills_per_image(tier: VideoModelPricingTier) -> UsdMills {
|
||||
match tier {
|
||||
VideoModelPricingTier::V1 => INPUT_MILLS_PER_IMAGE,
|
||||
VideoModelPricingTier::V1p5Preview => INPUT_MILLS_PER_IMAGE_V1P5,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,8 +273,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_is_independent_of_model_variant() {
|
||||
// All `model` enum variants map to the same pricing (only one video model).
|
||||
fn cost_within_v1_tier_is_independent_of_model_variant() {
|
||||
// Within the v1 pricing tier (default + unrecognized Custom strings),
|
||||
// model variant must not affect cost.
|
||||
let mut base = make_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
|
||||
let base_cost = base.calculate_cost_in_mills();
|
||||
for m in [
|
||||
@@ -250,6 +288,132 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
// ── grok-imagine-video-1.5-preview pricing ──
|
||||
//
|
||||
// v1.5 rates per the model's docs page:
|
||||
// 480p: 80 mills/sec, 720p: 140 mills/sec, 10 mills per source image.
|
||||
|
||||
mod v1p5_preview {
|
||||
use super::*;
|
||||
|
||||
fn make_v1p5_request(
|
||||
duration: Option<u32>,
|
||||
resolution: Option<VideoResolution>,
|
||||
image: Option<VideoImageSource>,
|
||||
reference_images: Option<Vec<VideoImageSource>>,
|
||||
) -> VideoGenerationRequest {
|
||||
let mut req = make_request(duration, resolution, image, reference_images);
|
||||
req.model = Some(VideoModel::GrokImagineVideo1p5Preview);
|
||||
req
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_seconds_480p_text_only() {
|
||||
// 80 × 5 = 400 mills = 40¢
|
||||
let req = make_v1p5_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 400);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_seconds_720p_text_only() {
|
||||
// 140 × 5 = 700 mills = 70¢
|
||||
let req = make_v1p5_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 700);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_seconds_720p_one_source_image() {
|
||||
// 140 × 5 + 10 × 1 = 710 mills = 71¢
|
||||
let req = make_v1p5_request(Some(5), Some(VideoResolution::SevenTwentyP), Some(url_source()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 710);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 71);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn five_seconds_480p_three_reference_images() {
|
||||
// 80 × 5 + 10 × 3 = 430 mills = 43¢
|
||||
let req = make_v1p5_request(
|
||||
Some(5), Some(VideoResolution::FourEightyP),
|
||||
None, Some(vec![url_source(), url_source(), url_source()]),
|
||||
);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 430);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 43);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dated_alias_via_custom_is_billed_as_v1p5() {
|
||||
// xAI accepts the dated alias as a synonym; cost must match the
|
||||
// canonical 1.5 variant exactly.
|
||||
let canonical = {
|
||||
let mut req = make_v1p5_request(Some(5), Some(VideoResolution::SevenTwentyP), Some(url_source()), None);
|
||||
req.model = Some(VideoModel::GrokImagineVideo1p5Preview);
|
||||
req.calculate_cost_in_mills()
|
||||
};
|
||||
let via_alias = {
|
||||
let mut req = make_v1p5_request(Some(5), Some(VideoResolution::SevenTwentyP), Some(url_source()), None);
|
||||
req.model = Some(VideoModel::Custom("grok-imagine-video-1.5-2026-05-30".to_string()));
|
||||
req.calculate_cost_in_mills()
|
||||
};
|
||||
assert_eq!(canonical, via_alias);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_with_canonical_1p5_name_is_billed_as_v1p5() {
|
||||
let via_custom = {
|
||||
let mut req = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None);
|
||||
req.model = Some(VideoModel::Custom("grok-imagine-video-1.5-preview".to_string()));
|
||||
req.calculate_cost_in_mills()
|
||||
};
|
||||
let via_enum = make_v1p5_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None)
|
||||
.calculate_cost_in_mills();
|
||||
assert_eq!(via_custom, via_enum);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v1p5_costs_more_than_v1_at_same_settings() {
|
||||
let v1 = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None).calculate_cost_in_mills();
|
||||
let v1p5 = make_v1p5_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None).calculate_cost_in_mills();
|
||||
assert!(v1 < v1p5, "v1={v1} v1p5={v1p5}");
|
||||
}
|
||||
|
||||
// (duration, resolution, has_image, ref_count, expected_mills)
|
||||
const V1P5_CASES: &[(u32, VideoResolution, bool, usize, u64)] = &[
|
||||
// 480p text-only
|
||||
( 1, VideoResolution::FourEightyP, false, 0, 80),
|
||||
( 8, VideoResolution::FourEightyP, false, 0, 640),
|
||||
(15, VideoResolution::FourEightyP, false, 0, 1200),
|
||||
// 720p text-only
|
||||
( 1, VideoResolution::SevenTwentyP, false, 0, 140),
|
||||
( 8, VideoResolution::SevenTwentyP, false, 0, 1120),
|
||||
(15, VideoResolution::SevenTwentyP, false, 0, 2100),
|
||||
// 480p with image-to-video
|
||||
( 5, VideoResolution::FourEightyP, true, 0, 410),
|
||||
(10, VideoResolution::FourEightyP, true, 0, 810),
|
||||
// 720p with image-to-video
|
||||
( 5, VideoResolution::SevenTwentyP, true, 0, 710),
|
||||
// 720p with reference images
|
||||
( 5, VideoResolution::SevenTwentyP, false, 2, 720),
|
||||
( 5, VideoResolution::SevenTwentyP, false, 3, 730),
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn v1p5_matrix() {
|
||||
for &(duration, res, has_image, ref_count, expected) in V1P5_CASES {
|
||||
let image = if has_image { Some(url_source()) } else { None };
|
||||
let refs = if ref_count > 0 {
|
||||
Some((0..ref_count).map(|_| url_source()).collect())
|
||||
} else { None };
|
||||
let req = make_v1p5_request(Some(duration), Some(res), image, refs);
|
||||
assert_eq!(
|
||||
req.calculate_cost_in_mills(), expected,
|
||||
"duration={duration} res={res:?} has_image={has_image} ref_count={ref_count}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Exhaustive matrix ──
|
||||
|
||||
mod exhaustive_matrix {
|
||||
|
||||
+17
-1
@@ -169,6 +169,21 @@ mod tests {
|
||||
assert!(result.video.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pending_response_with_v1p5_model_passes_model_string_through() {
|
||||
// The `model` field is opaque Option<String>; the v1.5 preview model
|
||||
// identifier (and its dated alias) flow through unchanged.
|
||||
let json = r#"{ "status": "pending", "progress": 5, "model": "grok-imagine-video-1.5-preview" }"#;
|
||||
let parsed: VideoStatusResponseBody = serde_json::from_str(json).unwrap();
|
||||
let result = classify_status_field(&parsed).unwrap();
|
||||
assert_eq!(result.model.as_deref(), Some("grok-imagine-video-1.5-preview"));
|
||||
|
||||
let json_alias = r#"{ "status": "pending", "progress": 5, "model": "grok-imagine-video-1.5-2026-05-30" }"#;
|
||||
let parsed_alias: VideoStatusResponseBody = serde_json::from_str(json_alias).unwrap();
|
||||
let result_alias = classify_status_field(&parsed_alias).unwrap();
|
||||
assert_eq!(result_alias.model.as_deref(), Some("grok-imagine-video-1.5-2026-05-30"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn done_response_classifies_as_done_with_video() {
|
||||
let json = r#"{
|
||||
@@ -291,7 +306,8 @@ mod tests {
|
||||
let result = video_status(VideoStatusArgs {
|
||||
api_key: &api_key,
|
||||
request: VideoStatusRequest {
|
||||
request_id: "e397ac83-c22f-90b1-9831-900c01497345".to_string(),
|
||||
//request_id: "e397ac83-c22f-90b1-9831-900c01497345".to_string(),
|
||||
request_id: "ce681bd0-133d-9cf3-975c-422d292d4e8e".to_string(),
|
||||
},
|
||||
}).await;
|
||||
|
||||
|
||||
@@ -13,10 +13,16 @@
|
||||
/// - <https://docs.x.ai/developers/model-capabilities/video/extension>
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum VideoModel {
|
||||
/// `grok-imagine-video` — the (currently sole) video model. Used by
|
||||
/// `grok-imagine-video` — the original Imagine video model. Used by
|
||||
/// generation, editing, and extension endpoints.
|
||||
GrokImagineVideo,
|
||||
|
||||
/// `grok-imagine-video-1.5-preview` — the 1.5 preview model. xAI also
|
||||
/// accepts the dated alias `grok-imagine-video-1.5-2026-05-30`, which
|
||||
/// resolves to the same model server-side; serialize via this variant for
|
||||
/// the canonical name.
|
||||
GrokImagineVideo1p5Preview,
|
||||
|
||||
/// Escape hatch for model identifiers not yet enumerated here.
|
||||
Custom(String),
|
||||
}
|
||||
@@ -25,10 +31,40 @@ impl VideoModel {
|
||||
/// Wire representation — the exact string xAI expects in the `"model"` field.
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::GrokImagineVideo => "grok-imagine-video",
|
||||
Self::Custom(s) => s.as_str(),
|
||||
Self::GrokImagineVideo => "grok-imagine-video",
|
||||
Self::GrokImagineVideo1p5Preview => "grok-imagine-video-1.5-preview",
|
||||
Self::Custom(s) => s.as_str(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Pricing tier this model bills under. Includes recognition of the dated
|
||||
/// alias `grok-imagine-video-1.5-2026-05-30` passed via [`Self::Custom`],
|
||||
/// which xAI treats as a synonym for `grok-imagine-video-1.5-preview` and
|
||||
/// therefore charges at the 1.5 rates.
|
||||
pub fn pricing_tier(&self) -> VideoModelPricingTier {
|
||||
match self {
|
||||
Self::GrokImagineVideo => VideoModelPricingTier::V1,
|
||||
Self::GrokImagineVideo1p5Preview => VideoModelPricingTier::V1p5Preview,
|
||||
Self::Custom(s) => match s.as_str() {
|
||||
"grok-imagine-video-1.5-preview" | "grok-imagine-video-1.5-2026-05-30" => {
|
||||
VideoModelPricingTier::V1p5Preview
|
||||
}
|
||||
_ => VideoModelPricingTier::V1,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Pricing tier identifier used by the per-request cost calculators. xAI
|
||||
/// publishes different per-second and per-image rates for each tier — see
|
||||
/// `video_generation/cost.rs`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum VideoModelPricingTier {
|
||||
/// `grok-imagine-video` and any unrecognized identifier that defaults
|
||||
/// to v1 rates.
|
||||
V1,
|
||||
/// `grok-imagine-video-1.5-preview` (and its dated alias).
|
||||
V1p5Preview,
|
||||
}
|
||||
|
||||
// Serialize as the wire string ("grok-imagine-video" or the Custom inner
|
||||
@@ -50,6 +86,18 @@ mod tests {
|
||||
assert_eq!(VideoModel::GrokImagineVideo.as_str(), "grok-imagine-video");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_point_five_preview_serializes_canonical_name() {
|
||||
assert_eq!(
|
||||
VideoModel::GrokImagineVideo1p5Preview.as_str(),
|
||||
"grok-imagine-video-1.5-preview",
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&VideoModel::GrokImagineVideo1p5Preview).unwrap(),
|
||||
"\"grok-imagine-video-1.5-preview\"",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_model_passes_through() {
|
||||
let m = VideoModel::Custom("grok-imagine-video-future".to_string());
|
||||
@@ -62,4 +110,48 @@ mod tests {
|
||||
let m = VideoModel::Custom("grok-imagine-video-v2".to_string());
|
||||
assert_eq!(serde_json::to_string(&m).unwrap(), "\"grok-imagine-video-v2\"");
|
||||
}
|
||||
|
||||
// ── Pricing tier classification ──
|
||||
|
||||
mod pricing_tier {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn grok_imagine_video_is_v1() {
|
||||
assert_eq!(VideoModel::GrokImagineVideo.pricing_tier(), VideoModelPricingTier::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_point_five_preview_enum_is_v1p5() {
|
||||
assert_eq!(
|
||||
VideoModel::GrokImagineVideo1p5Preview.pricing_tier(),
|
||||
VideoModelPricingTier::V1p5Preview,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_with_canonical_1p5_name_is_v1p5() {
|
||||
let m = VideoModel::Custom("grok-imagine-video-1.5-preview".to_string());
|
||||
assert_eq!(m.pricing_tier(), VideoModelPricingTier::V1p5Preview);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_with_dated_1p5_alias_is_v1p5() {
|
||||
// xAI treats this alias as a synonym for the 1.5 preview model.
|
||||
let m = VideoModel::Custom("grok-imagine-video-1.5-2026-05-30".to_string());
|
||||
assert_eq!(m.pricing_tier(), VideoModelPricingTier::V1p5Preview);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_unknown_defaults_to_v1() {
|
||||
let m = VideoModel::Custom("grok-imagine-video-future".to_string());
|
||||
assert_eq!(m.pricing_tier(), VideoModelPricingTier::V1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_with_v1_name_is_v1() {
|
||||
let m = VideoModel::Custom("grok-imagine-video".to_string());
|
||||
assert_eq!(m.pricing_tier(), VideoModelPricingTier::V1);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -77,6 +77,7 @@ fn video_model_to_common_model_type(model: RouterVideoModel) -> Option<CommonMod
|
||||
RouterVideoModel::PreviewModel => Some(CommonModelType::PreviewModel),
|
||||
RouterVideoModel::PreviewModelFast => Some(CommonModelType::PreviewModelFast),
|
||||
RouterVideoModel::GrokImagineVideo => Some(CommonModelType::GrokImagineVideo),
|
||||
RouterVideoModel::GrokImagineVideo1p5 => Some(CommonModelType::GrokImagineVideo1p5),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -149,6 +149,8 @@ pub enum InferenceModelType {
|
||||
GrokVideo,
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
@@ -286,6 +288,7 @@ impl InferenceModelType {
|
||||
// Video models
|
||||
Self::GrokVideo => "grok_video",
|
||||
Self::GrokImagineVideo => "grok_imagine_video",
|
||||
Self::GrokImagineVideo1p5 => "grok_imagine_video_1p5",
|
||||
Self::Kling16Pro => "kling_1p6_pro",
|
||||
Self::Kling21Pro => "kling_2p1_pro",
|
||||
Self::Kling21Master => "kling_2p1_master",
|
||||
@@ -384,6 +387,7 @@ impl InferenceModelType {
|
||||
// Video models
|
||||
"grok_video" => Ok(Self::GrokVideo),
|
||||
"grok_imagine_video" => Ok(Self::GrokImagineVideo),
|
||||
"grok_imagine_video_1p5" => Ok(Self::GrokImagineVideo1p5),
|
||||
"kling_1p6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2p1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2p1_master" => Ok(Self::Kling21Master),
|
||||
@@ -486,6 +490,7 @@ impl InferenceModelType {
|
||||
// Video models
|
||||
Self::GrokVideo,
|
||||
Self::GrokImagineVideo,
|
||||
Self::GrokImagineVideo1p5,
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
@@ -571,6 +576,7 @@ impl InferenceModelType {
|
||||
// Video models
|
||||
CommonModelType::GrokVideo => Self::GrokVideo,
|
||||
CommonModelType::GrokImagineVideo => Self::GrokImagineVideo,
|
||||
CommonModelType::GrokImagineVideo1p5 => Self::GrokImagineVideo1p5,
|
||||
CommonModelType::Kling16Pro => Self::Kling16Pro,
|
||||
CommonModelType::Kling21Pro => Self::Kling21Pro,
|
||||
CommonModelType::Kling21Master => Self::Kling21Master,
|
||||
@@ -679,6 +685,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_serialization(InferenceModelType::GrokVideo, "grok_video");
|
||||
assert_serialization(InferenceModelType::GrokImagineVideo, "grok_imagine_video");
|
||||
assert_serialization(InferenceModelType::GrokImagineVideo1p5, "grok_imagine_video_1p5");
|
||||
assert_serialization(InferenceModelType::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(InferenceModelType::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(InferenceModelType::Kling21Master, "kling_2p1_master");
|
||||
@@ -776,6 +783,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_eq!(InferenceModelType::GrokVideo.to_str(), "grok_video");
|
||||
assert_eq!(InferenceModelType::GrokImagineVideo.to_str(), "grok_imagine_video");
|
||||
assert_eq!(InferenceModelType::GrokImagineVideo1p5.to_str(), "grok_imagine_video_1p5");
|
||||
assert_eq!(InferenceModelType::Kling16Pro.to_str(), "kling_1p6_pro");
|
||||
assert_eq!(InferenceModelType::Kling21Pro.to_str(), "kling_2p1_pro");
|
||||
assert_eq!(InferenceModelType::Kling21Master.to_str(), "kling_2p1_master");
|
||||
@@ -873,6 +881,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_eq!(InferenceModelType::from_str("grok_video").unwrap(), InferenceModelType::GrokVideo);
|
||||
assert_eq!(InferenceModelType::from_str("grok_imagine_video").unwrap(), InferenceModelType::GrokImagineVideo);
|
||||
assert_eq!(InferenceModelType::from_str("grok_imagine_video_1p5").unwrap(), InferenceModelType::GrokImagineVideo1p5);
|
||||
assert_eq!(InferenceModelType::from_str("kling_1p6_pro").unwrap(), InferenceModelType::Kling16Pro);
|
||||
assert_eq!(InferenceModelType::from_str("kling_2p1_pro").unwrap(), InferenceModelType::Kling21Pro);
|
||||
assert_eq!(InferenceModelType::from_str("kling_2p1_master").unwrap(), InferenceModelType::Kling21Master);
|
||||
|
||||
@@ -117,6 +117,9 @@ pub enum CommonModelType {
|
||||
/// No version is specified.
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
/// xAI's Grok Imagine video 1.5 preview model (grok-imagine-video-1.5-preview).
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
@@ -238,6 +241,7 @@ impl CommonModelType {
|
||||
// Video models
|
||||
Self::GrokVideo => "grok_video",
|
||||
Self::GrokImagineVideo => "grok_imagine_video",
|
||||
Self::GrokImagineVideo1p5 => "grok_imagine_video_1p5",
|
||||
Self::Kling16Pro => "kling_1p6_pro",
|
||||
Self::Kling21Pro => "kling_2p1_pro",
|
||||
Self::Kling21Master => "kling_2p1_master",
|
||||
@@ -319,6 +323,7 @@ impl CommonModelType {
|
||||
// Video models
|
||||
"grok_video" => Ok(Self::GrokVideo),
|
||||
"grok_imagine_video" => Ok(Self::GrokImagineVideo),
|
||||
"grok_imagine_video_1p5" => Ok(Self::GrokImagineVideo1p5),
|
||||
"kling_1p6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2p1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2p1_master" => Ok(Self::Kling21Master),
|
||||
@@ -404,6 +409,7 @@ impl CommonModelType {
|
||||
// Video models
|
||||
Self::GrokVideo,
|
||||
Self::GrokImagineVideo,
|
||||
Self::GrokImagineVideo1p5,
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
@@ -486,6 +492,7 @@ impl CommonModelType {
|
||||
// Video models
|
||||
Self::GrokVideo => CommonModelClass::Video,
|
||||
Self::GrokImagineVideo => CommonModelClass::Video,
|
||||
Self::GrokImagineVideo1p5 => CommonModelClass::Video,
|
||||
Self::Kling16Pro => CommonModelClass::Video,
|
||||
Self::Kling21Pro => CommonModelClass::Video,
|
||||
Self::Kling21Master => CommonModelClass::Video,
|
||||
@@ -577,6 +584,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_serialization(CommonModelType::GrokVideo, "grok_video");
|
||||
assert_serialization(CommonModelType::GrokImagineVideo, "grok_imagine_video");
|
||||
assert_serialization(CommonModelType::GrokImagineVideo1p5, "grok_imagine_video_1p5");
|
||||
assert_serialization(CommonModelType::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(CommonModelType::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(CommonModelType::Kling21Master, "kling_2p1_master");
|
||||
@@ -655,6 +663,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_eq!(CommonModelType::GrokVideo.to_str(), "grok_video");
|
||||
assert_eq!(CommonModelType::GrokImagineVideo.to_str(), "grok_imagine_video");
|
||||
assert_eq!(CommonModelType::GrokImagineVideo1p5.to_str(), "grok_imagine_video_1p5");
|
||||
assert_eq!(CommonModelType::Kling16Pro.to_str(), "kling_1p6_pro");
|
||||
assert_eq!(CommonModelType::Kling21Pro.to_str(), "kling_2p1_pro");
|
||||
assert_eq!(CommonModelType::Kling21Master.to_str(), "kling_2p1_master");
|
||||
@@ -733,6 +742,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_eq!(CommonModelType::from_str("grok_video").unwrap(), CommonModelType::GrokVideo);
|
||||
assert_eq!(CommonModelType::from_str("grok_imagine_video").unwrap(), CommonModelType::GrokImagineVideo);
|
||||
assert_eq!(CommonModelType::from_str("grok_imagine_video_1p5").unwrap(), CommonModelType::GrokImagineVideo1p5);
|
||||
assert_eq!(CommonModelType::from_str("kling_1p6_pro").unwrap(), CommonModelType::Kling16Pro);
|
||||
assert_eq!(CommonModelType::from_str("kling_2p1_pro").unwrap(), CommonModelType::Kling21Pro);
|
||||
assert_eq!(CommonModelType::from_str("kling_2p1_master").unwrap(), CommonModelType::Kling21Master);
|
||||
@@ -775,7 +785,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonModelType::all_variants();
|
||||
assert_eq!(variants.len(), 69);
|
||||
assert_eq!(variants.len(), 70);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||
@@ -813,6 +823,7 @@ mod tests {
|
||||
// Video models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokVideo));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokImagineVideo));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokImagineVideo1p5));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling16Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling21Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling21Master));
|
||||
|
||||
@@ -14,6 +14,9 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
|
||||
#[serde(rename = "grok_imagine_video_1p5")]
|
||||
GrokImagineVideo1p5,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
@@ -106,6 +109,7 @@ impl CommonVideoModel {
|
||||
match self {
|
||||
Self::GrokVideo => CommonModelType::GrokVideo,
|
||||
Self::GrokImagineVideo => CommonModelType::GrokImagineVideo,
|
||||
Self::GrokImagineVideo1p5 => CommonModelType::GrokImagineVideo1p5,
|
||||
Self::Kling16Pro => CommonModelType::Kling16Pro,
|
||||
Self::Kling21Pro => CommonModelType::Kling21Pro,
|
||||
Self::Kling21Master => CommonModelType::Kling21Master,
|
||||
@@ -147,6 +151,7 @@ mod tests {
|
||||
fn test_serialization() {
|
||||
assert_serialization(CommonVideoModel::GrokVideo, "grok_video");
|
||||
assert_serialization(CommonVideoModel::GrokImagineVideo, "grok_imagine_video");
|
||||
assert_serialization(CommonVideoModel::GrokImagineVideo1p5, "grok_imagine_video_1p5");
|
||||
assert_serialization(CommonVideoModel::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(CommonVideoModel::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(CommonVideoModel::Kling21Master, "kling_2p1_master");
|
||||
@@ -181,6 +186,7 @@ mod tests {
|
||||
let cases = [
|
||||
("grok_video", CommonVideoModel::GrokVideo),
|
||||
("grok_imagine_video", CommonVideoModel::GrokImagineVideo),
|
||||
("grok_imagine_video_1p5", CommonVideoModel::GrokImagineVideo1p5),
|
||||
("kling_1p6_pro", CommonVideoModel::Kling16Pro),
|
||||
("kling_2p1_pro", CommonVideoModel::Kling21Pro),
|
||||
("kling_2p1_master", CommonVideoModel::Kling21Master),
|
||||
@@ -222,6 +228,7 @@ mod tests {
|
||||
let all = [
|
||||
CommonVideoModel::GrokVideo,
|
||||
CommonVideoModel::GrokImagineVideo,
|
||||
CommonVideoModel::GrokImagineVideo1p5,
|
||||
CommonVideoModel::Kling16Pro,
|
||||
CommonVideoModel::Kling21Pro,
|
||||
CommonVideoModel::Kling21Master,
|
||||
@@ -262,6 +269,7 @@ mod tests {
|
||||
let models = [
|
||||
(CommonVideoModel::GrokVideo, CommonModelType::GrokVideo),
|
||||
(CommonVideoModel::GrokImagineVideo, CommonModelType::GrokImagineVideo),
|
||||
(CommonVideoModel::GrokImagineVideo1p5, CommonModelType::GrokImagineVideo1p5),
|
||||
(CommonVideoModel::Kling16Pro, CommonModelType::Kling16Pro),
|
||||
(CommonVideoModel::Kling21Pro, CommonModelType::Kling21Pro),
|
||||
(CommonVideoModel::Kling21Master, CommonModelType::Kling21Master),
|
||||
|
||||
@@ -55,6 +55,44 @@ fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::GrokImagineVideo1p5,
|
||||
model_creator: Some(ModelCreator::Grok),
|
||||
full_name: Some("Grok Imagine 1.5 Preview".to_string()),
|
||||
extra_info: Some("Fast and high quality".to_string()),
|
||||
extra_info_short: Some("Fast and high quality".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(false),
|
||||
//image_references_max: Some(7),
|
||||
//video_references_supported: Some(false),
|
||||
//video_references_max: Some(3),
|
||||
//video_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
//batch_size_options: Some(vec![1, 2, 4]),
|
||||
//batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(1),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_max_with_image_references: Some(10),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::HappyHorse1p0,
|
||||
model_creator: Some(ModelCreator::Alibaba),
|
||||
|
||||
+1
@@ -131,6 +131,7 @@ fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerE
|
||||
let router_model = match model {
|
||||
CommonVideoModel::GrokVideo => RouterVideoModel::GrokVideo,
|
||||
CommonVideoModel::GrokImagineVideo => RouterVideoModel::GrokImagineVideo,
|
||||
CommonVideoModel::GrokImagineVideo1p5 => RouterVideoModel::GrokImagineVideo1p5,
|
||||
CommonVideoModel::Kling16Pro => RouterVideoModel::Kling16Pro,
|
||||
CommonVideoModel::Kling21Pro => RouterVideoModel::Kling21Pro,
|
||||
CommonVideoModel::Kling21Master => RouterVideoModel::Kling21Master,
|
||||
|
||||
+1
@@ -65,6 +65,7 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
//RouterVideoModel::Seedance2p0Ultra => RouterProvider::GmiCloud,
|
||||
//RouterVideoModel::Seedance2p0UltraFast => RouterProvider::GmiCloud,
|
||||
RouterVideoModel::GrokImagineVideo => RouterProvider::GrokApi,
|
||||
RouterVideoModel::GrokImagineVideo1p5 => RouterProvider::GrokApi,
|
||||
_ => RouterProvider::Fal,
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user