mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 17:58:55 +00:00
cost edits
This commit is contained in:
1 parent
4ab13bb11a
commit
a59561cddf
6 files changed
+640
-150
No files matched your search
@@ -1,55 +1,83 @@
|
||||
use crate::api::requests::videos::video_edit::video_edit::VideoEditRequest;
|
||||
use crate::api::requests::videos::video_generation::cost::{
|
||||
output_mills_per_second,
|
||||
DEFAULT_VIDEO_DURATION_SECONDS,
|
||||
INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO,
|
||||
};
|
||||
use crate::api::traits::grok_request_cost_calculator_trait::{GrokRequestCostCalculator, UsdMills};
|
||||
use crate::api::traits::grok_request_source_duration_cost_calculator_trait::GrokRequestSourceDurationCostCalculator;
|
||||
use crate::api::types::video_types::video_resolution::VideoResolution;
|
||||
|
||||
// `video_edit` cost depends entirely on the source video's duration:
|
||||
/// Fallback resolution used by the base [`GrokRequestCostCalculator`] impl
|
||||
/// when the caller hasn't supplied the source's actual resolution. Picks
|
||||
/// the more expensive tier (720p) so estimates don't undershoot.
|
||||
const ASSUMED_SOURCE_RESOLUTION: VideoResolution = VideoResolution::SevenTwentyP;
|
||||
|
||||
// `video_edit` cost depends entirely on the source video's properties:
|
||||
//
|
||||
// Input: $0.01/sec × source_duration (10 mills/sec)
|
||||
// Output: $0.07/sec × source_duration (70 mills/sec, since
|
||||
// output mirrors the source resolution capped at 720p, and
|
||||
// output duration mirrors source duration per xAI docs)
|
||||
// Input: 10 mills/sec × source_duration (resolution-independent)
|
||||
// Output: output_mills_per_second(res) × source_duration
|
||||
// - 480p source → 50 mills/sec output
|
||||
// - 720p source → 70 mills/sec output
|
||||
//
|
||||
// Neither piece is knowable from the `VideoEditRequest` alone — the
|
||||
// `source_video` field only points at a URL/file_id, not at metadata. The
|
||||
// trait impl therefore returns 0 so callers don't get a misleading bound.
|
||||
// xAI documents that video edits re-render the ENTIRE source video — output
|
||||
// duration mirrors source duration, output resolution mirrors source
|
||||
// resolution (capped at 720p, which is also our max-supported variant).
|
||||
//
|
||||
// If you need a real estimate, fetch the source's metadata out-of-band
|
||||
// (e.g. `ffprobe` for a local file, or HEAD + Range read for a remote MP4)
|
||||
// and compute:
|
||||
// Neither source duration nor source resolution is in the request body — the
|
||||
// `source_video` field only points at a URL/file_id.
|
||||
//
|
||||
// mills = source_duration_seconds * (INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO
|
||||
// + output_mills_per_second(VideoResolution::SevenTwentyP))
|
||||
// = source_duration_seconds * (10 + 70)
|
||||
// = source_duration_seconds * 80
|
||||
// Two impls cover the cost-estimation gradient:
|
||||
//
|
||||
// - `GrokRequestCostCalculator::calculate_cost_in_mills` falls back to a
|
||||
// conservative default (8s source @ 720p, matching xAI's
|
||||
// /v1/videos/generations default) so callers without source metadata
|
||||
// get a non-zero estimate. Returning 0 would silently misrepresent the
|
||||
// cost as free.
|
||||
//
|
||||
// - `GrokRequestSourceDurationCostCalculator::calculate_cost_in_mills_with_source_duration`
|
||||
// takes the actual source duration AND resolution from the caller
|
||||
// (typically via ffprobe or an upstream generate call) and returns the
|
||||
// exact billed amount.
|
||||
|
||||
impl GrokRequestCostCalculator for VideoEditRequest {
|
||||
/// **Always returns 0.** Cost cannot be computed from the request body
|
||||
/// — see the module-level comment for the formula you'd apply if you
|
||||
/// have the source video's duration available out-of-band.
|
||||
/// Uses [`VideoEditRequest::source_video_duration_seconds`] when set;
|
||||
/// otherwise falls back to a conservative default ([`DEFAULT_VIDEO_DURATION_SECONDS`]
|
||||
/// = 8s, matching xAI's `/v1/videos/generations` default).
|
||||
///
|
||||
/// If you DO have the source duration, use
|
||||
/// Source resolution is always assumed to be 720p (the upper-tier rate,
|
||||
/// picked so estimates don't undershoot) since the request can't carry
|
||||
/// it. If you need to override the resolution assumption, call
|
||||
/// [`GrokRequestSourceDurationCostCalculator::calculate_cost_in_mills_with_source_duration`]
|
||||
/// instead.
|
||||
/// directly.
|
||||
///
|
||||
/// Formula: `secs × (10 input + 70 output@720p) = 80 × secs` mills.
|
||||
///
|
||||
/// At the 8-second default: 8 × 80 = **640 mills (64¢)**.
|
||||
fn calculate_cost_in_mills(&self) -> UsdMills {
|
||||
0
|
||||
let secs = self.source_video_duration_seconds.unwrap_or(DEFAULT_VIDEO_DURATION_SECONDS);
|
||||
self.calculate_cost_in_mills_with_source_duration(secs, ASSUMED_SOURCE_RESOLUTION)
|
||||
}
|
||||
}
|
||||
|
||||
impl GrokRequestSourceDurationCostCalculator for VideoEditRequest {
|
||||
/// Output duration mirrors the source, output resolution mirrors the
|
||||
/// source capped at 720p. Both input and output are billed per second of
|
||||
/// source duration:
|
||||
/// Both input and output are billed per second of source duration; output
|
||||
/// rate depends on source resolution (which equals output resolution for
|
||||
/// video edits, capped at 720p).
|
||||
///
|
||||
/// total = source_duration_seconds × (INPUT + OUTPUT_720P)
|
||||
/// = source_duration_seconds × (10 + 70) = 80 mills/sec
|
||||
fn calculate_cost_in_mills_with_source_duration(&self, source_duration_seconds: u32) -> UsdMills {
|
||||
/// total = source_duration_seconds × (10 mills/sec input
|
||||
/// + output_mills_per_second(res))
|
||||
///
|
||||
/// Examples:
|
||||
/// - 480p source: (10 + 50) × secs = 60 mills/sec
|
||||
/// - 720p source: (10 + 70) × secs = 80 mills/sec
|
||||
fn calculate_cost_in_mills_with_source_duration(
|
||||
&self,
|
||||
source_duration_seconds: u32,
|
||||
source_resolution: VideoResolution,
|
||||
) -> UsdMills {
|
||||
let secs = source_duration_seconds as u64;
|
||||
let per_second = INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO + output_mills_per_second(VideoResolution::SevenTwentyP);
|
||||
let per_second = INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO + output_mills_per_second(source_resolution);
|
||||
per_second * secs
|
||||
}
|
||||
}
|
||||
@@ -64,41 +92,137 @@ mod tests {
|
||||
VideoEditRequest {
|
||||
prompt: "test edit".to_string(),
|
||||
source_video,
|
||||
source_video_duration_seconds: None,
|
||||
model,
|
||||
user: None,
|
||||
}
|
||||
}
|
||||
|
||||
// Default base-trait estimate assumes 8s source @ 720p, so:
|
||||
// 8 × (10 input + 70 output) = 640 mills = 64¢
|
||||
const EXPECTED_BASE_ESTIMATE_MILLS: u64 = 640;
|
||||
const EXPECTED_BASE_ESTIMATE_CENTS: u64 = 64;
|
||||
|
||||
#[test]
|
||||
fn always_returns_zero() {
|
||||
fn base_estimate_is_conservative_eight_seconds_seven_twenty_p() {
|
||||
let cases = [
|
||||
VideoSource::Url("https://example.com/v.mp4".to_string()),
|
||||
VideoSource::FileId("file_abc".to_string()),
|
||||
];
|
||||
for source in cases {
|
||||
let req = make_request(source, None);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 0);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 0);
|
||||
assert_eq!(req.calculate_cost_in_mills(), EXPECTED_BASE_ESTIMATE_MILLS);
|
||||
assert_eq!(req.calculate_cost_in_cents(), EXPECTED_BASE_ESTIMATE_CENTS);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_regardless_of_model() {
|
||||
let req_default = make_request(VideoSource::Url("u".to_string()), None);
|
||||
let req_known = make_request(VideoSource::Url("u".to_string()), Some(VideoModel::GrokImagineVideo));
|
||||
let req_custom = make_request(VideoSource::Url("u".to_string()),
|
||||
Some(VideoModel::Custom("future".to_string())));
|
||||
assert_eq!(req_default.calculate_cost_in_mills(), 0);
|
||||
assert_eq!(req_known.calculate_cost_in_mills(), 0);
|
||||
assert_eq!(req_custom.calculate_cost_in_mills(), 0);
|
||||
fn base_estimate_is_independent_of_model() {
|
||||
let r_default = make_request(VideoSource::Url("u".to_string()), None).calculate_cost_in_mills();
|
||||
let r_known = make_request(VideoSource::Url("u".to_string()), Some(VideoModel::GrokImagineVideo)).calculate_cost_in_mills();
|
||||
let r_custom = make_request(VideoSource::Url("u".to_string()),
|
||||
Some(VideoModel::Custom("future".to_string()))).calculate_cost_in_mills();
|
||||
assert_eq!(r_default, EXPECTED_BASE_ESTIMATE_MILLS);
|
||||
assert_eq!(r_known, EXPECTED_BASE_ESTIMATE_MILLS);
|
||||
assert_eq!(r_custom, EXPECTED_BASE_ESTIMATE_MILLS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_regardless_of_prompt_or_user() {
|
||||
fn base_estimate_is_independent_of_prompt_or_user() {
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.prompt = "an enormously long prompt with many tokens".repeat(100);
|
||||
req.user = Some("user-id".to_string());
|
||||
assert_eq!(req.calculate_cost_in_mills(), 0);
|
||||
assert_eq!(req.calculate_cost_in_mills(), EXPECTED_BASE_ESTIMATE_MILLS);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_estimate_equals_source_duration_method_with_defaults() {
|
||||
// The base impl must agree with the source-duration-aware impl when
|
||||
// called with the documented default assumptions (8s, 720p).
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(
|
||||
req.calculate_cost_in_mills(),
|
||||
req.calculate_cost_in_mills_with_source_duration(8, VideoResolution::SevenTwentyP),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_estimate_is_nonzero() {
|
||||
// Sanity guard against accidentally regressing to the old return-0 footgun.
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert!(req.calculate_cost_in_mills() > 0);
|
||||
assert!(req.calculate_cost_in_cents() > 0);
|
||||
}
|
||||
|
||||
// ── source_video_duration_seconds hint ──
|
||||
|
||||
mod duration_hint {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn hint_overrides_default_duration() {
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.source_video_duration_seconds = Some(5);
|
||||
// 5 × 80 = 400 mills (vs the 640 mills default)
|
||||
assert_eq!(req.calculate_cost_in_mills(), 400);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_second_hint() {
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.source_video_duration_seconds = Some(1);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 80); // 1 × 80
|
||||
assert_eq!(req.calculate_cost_in_cents(), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifteen_second_hint() {
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.source_video_duration_seconds = Some(15);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 1200); // 15 × 80
|
||||
assert_eq!(req.calculate_cost_in_cents(), 120);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_matches_explicit_source_duration_method_at_720p() {
|
||||
// The hint-driven base method should agree with the explicit
|
||||
// source-duration method at the assumed (720p) resolution.
|
||||
for secs in 0u32..=30 {
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.source_video_duration_seconds = Some(secs);
|
||||
assert_eq!(
|
||||
req.calculate_cost_in_mills(),
|
||||
req.calculate_cost_in_mills_with_source_duration(secs, VideoResolution::SevenTwentyP),
|
||||
"secs={secs}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unset_hint_falls_back_to_default() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.source_video_duration_seconds, None);
|
||||
// Default = 8s @ 720p = 640 mills.
|
||||
assert_eq!(req.calculate_cost_in_mills(), 640);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_second_hint_costs_zero() {
|
||||
// Edge case: 0-second source is meaningless but shouldn't panic.
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.source_video_duration_seconds = Some(0);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_scales_linearly() {
|
||||
for secs in 1u32..=20 {
|
||||
let mut req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
req.source_video_duration_seconds = Some(secs);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 80 * secs as u64, "secs={secs}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Source-duration-aware variant ──
|
||||
@@ -106,52 +230,138 @@ mod tests {
|
||||
mod with_source_duration {
|
||||
use super::*;
|
||||
|
||||
// ── 720p source (output capped at 720p, so 70 + 10 = 80 mills/sec) ──
|
||||
|
||||
mod source_720p {
|
||||
use super::*;
|
||||
const RES: VideoResolution = VideoResolution::SevenTwentyP;
|
||||
|
||||
#[test]
|
||||
fn five_second_source() {
|
||||
// 80 × 5 = 400 mills = 40¢
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5, RES), 400);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5, RES), 40);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_second_source() {
|
||||
// 80 × 1 = 80 mills = 8¢
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1, RES), 80);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1, RES), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_second_source_is_eighty_cents() {
|
||||
// 80 × 10 = 800 mills = 80¢
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10, RES), 800);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10, RES), 80);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scales_linearly() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
for secs in 1u32..=60 {
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs, RES), 80 * secs as u64, "secs={secs}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 480p source (output at 480p, so 50 + 10 = 60 mills/sec) ──
|
||||
|
||||
mod source_480p {
|
||||
use super::*;
|
||||
const RES: VideoResolution = VideoResolution::FourEightyP;
|
||||
|
||||
#[test]
|
||||
fn five_second_source() {
|
||||
// 60 × 5 = 300 mills = 30¢
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5, RES), 300);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5, RES), 30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_second_source() {
|
||||
// 60 × 1 = 60 mills = 6¢
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1, RES), 60);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1, RES), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_second_source_is_sixty_cents() {
|
||||
// 60 × 10 = 600 mills = 60¢
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10, RES), 600);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10, RES), 60);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scales_linearly() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
for secs in 1u32..=60 {
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs, RES), 60 * secs as u64, "secs={secs}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-resolution comparisons ──
|
||||
|
||||
#[test]
|
||||
fn five_second_source() {
|
||||
// 80 × 5 = 400 mills = 40¢
|
||||
fn zero_second_source_costs_zero_regardless_of_resolution() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5), 400);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5), 40);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0, VideoResolution::FourEightyP), 0);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0, VideoResolution::SevenTwentyP), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_second_source() {
|
||||
// 80 × 1 = 80 mills = 8¢
|
||||
fn higher_resolution_costs_more() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1), 80);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1), 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_second_source_costs_zero() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scales_linearly_with_source_duration() {
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
for secs in 1u32..=60 {
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs), 80 * secs as u64, "secs={secs}");
|
||||
for secs in 1u32..=15 {
|
||||
let lo = req.calculate_cost_in_mills_with_source_duration(secs, VideoResolution::FourEightyP);
|
||||
let hi = req.calculate_cost_in_mills_with_source_duration(secs, VideoResolution::SevenTwentyP);
|
||||
assert!(lo < hi, "secs={secs}: 480p ({lo}) should be < 720p ({hi})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_second_source_is_one_dollar() {
|
||||
// 80 × 10 = 800 mills = 80¢
|
||||
fn matrix_of_duration_and_resolution() {
|
||||
// (duration, resolution, expected_mills)
|
||||
let cases: &[(u32, VideoResolution, u64)] = &[
|
||||
( 1, VideoResolution::FourEightyP, 60),
|
||||
( 5, VideoResolution::FourEightyP, 300),
|
||||
(10, VideoResolution::FourEightyP, 600),
|
||||
(15, VideoResolution::FourEightyP, 900),
|
||||
(30, VideoResolution::FourEightyP, 1800),
|
||||
( 1, VideoResolution::SevenTwentyP, 80),
|
||||
( 5, VideoResolution::SevenTwentyP, 400),
|
||||
(10, VideoResolution::SevenTwentyP, 800),
|
||||
(15, VideoResolution::SevenTwentyP, 1200),
|
||||
(30, VideoResolution::SevenTwentyP, 2400),
|
||||
];
|
||||
let req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10), 800);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10), 80);
|
||||
for &(secs, res, expected) in cases {
|
||||
assert_eq!(
|
||||
req.calculate_cost_in_mills_with_source_duration(secs, res),
|
||||
expected,
|
||||
"secs={secs} res={res:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn independent_of_source_kind() {
|
||||
let url_req = make_request(VideoSource::Url("u".to_string()), None);
|
||||
let file_req = make_request(VideoSource::FileId("f".to_string()), None);
|
||||
assert_eq!(
|
||||
url_req.calculate_cost_in_mills_with_source_duration(7),
|
||||
file_req.calculate_cost_in_mills_with_source_duration(7),
|
||||
);
|
||||
for res in [VideoResolution::FourEightyP, VideoResolution::SevenTwentyP] {
|
||||
assert_eq!(
|
||||
url_req.calculate_cost_in_mills_with_source_duration(7, res),
|
||||
file_req.calculate_cost_in_mills_with_source_duration(7, res),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,18 @@ pub struct VideoEditRequest {
|
||||
/// Source video to modify (xAI reads this as the input).
|
||||
pub source_video: VideoSource,
|
||||
|
||||
/// **Cost-estimation hint — NOT sent to xAI, has zero effect on the
|
||||
/// actual HTTP request.** Duration in whole seconds of the video at
|
||||
/// [`source_video`](Self::source_video). When set, the
|
||||
/// [`crate::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator`]
|
||||
/// impl uses it for an accurate cost estimate; when `None`, the impl
|
||||
/// falls back to a conservative default (xAI's 8-second generation
|
||||
/// default). Populate this field if you already know the source's
|
||||
/// runtime (e.g. from ffprobe or an upstream generate call) so internal
|
||||
/// cost tracking stays correct.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_video_duration_seconds: Option<u32>,
|
||||
|
||||
/// Model identifier. Defaults to [`VideoModel::GrokImagineVideo`] when `None`.
|
||||
/// Use [`VideoModel::Custom`] for identifiers not yet listed in the enum.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -170,6 +182,7 @@ mod tests {
|
||||
request: VideoEditRequest {
|
||||
prompt: "p".to_string(),
|
||||
source_video: VideoSource::FileId("file_abc".to_string()),
|
||||
source_video_duration_seconds: None,
|
||||
model: None,
|
||||
user: None,
|
||||
},
|
||||
@@ -177,6 +190,35 @@ mod tests {
|
||||
let json = serde_json::to_string(&args.request).unwrap();
|
||||
assert!(!json.contains("secret_must_not_leak"));
|
||||
assert!(json.contains("\"source_video\":{\"FileId\":\"file_abc\"}"));
|
||||
// Hint omitted when None.
|
||||
assert!(!json.contains("source_video_duration_seconds"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_hint_serializes_when_set() {
|
||||
let req = VideoEditRequest {
|
||||
prompt: "p".to_string(),
|
||||
source_video: VideoSource::Url("u".to_string()),
|
||||
source_video_duration_seconds: Some(12),
|
||||
model: None,
|
||||
user: None,
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("\"source_video_duration_seconds\":12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_hint_is_not_in_wire_body() {
|
||||
// The internal wire body (what actually gets POSTed to xAI) doesn't
|
||||
// carry the hint — it's intentionally absent from VideoEditRequestBody.
|
||||
let body = VideoEditRequestBody {
|
||||
prompt: "p".to_string(),
|
||||
video: VideoSourceRef { url: Some("u".to_string()), file_id: None },
|
||||
model: None,
|
||||
user: None,
|
||||
};
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(!json.contains("source_video_duration_seconds"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -202,6 +244,7 @@ mod tests {
|
||||
request: VideoEditRequest {
|
||||
prompt: "Change the lighting to midnight. Starry sky with the mily way overhead. Add some shooting stars. Everything is glowing under the starlight.".to_string(),
|
||||
source_video: VideoSource::Url(ANGRY_SHIBA_VIDEO_URL.to_string()),
|
||||
source_video_duration_seconds: None,
|
||||
model: None,
|
||||
user: None,
|
||||
},
|
||||
|
||||
+233
-60
@@ -27,28 +27,47 @@ use crate::api::types::video_types::video_resolution::VideoResolution;
|
||||
const DEFAULT_EXTENSION_DURATION_SECONDS: u32 = 6;
|
||||
|
||||
impl GrokRequestCostCalculator for VideoExtensionRequest {
|
||||
/// Estimates the **output** portion only. The input portion depends on the
|
||||
/// source video's duration, which isn't in the request; callers should
|
||||
/// add it separately if they need a true total.
|
||||
/// Output portion is always known (extension duration × 70 mills/sec at
|
||||
/// the assumed 720p output).
|
||||
///
|
||||
/// If you DO have the source duration, use
|
||||
/// Input portion is included **only when**
|
||||
/// [`VideoExtensionRequest::source_video_duration_seconds`] is set —
|
||||
/// otherwise the source duration is unknown and we'd be guessing.
|
||||
///
|
||||
/// Formula:
|
||||
/// - hint unset: `extension_duration × 70`
|
||||
/// - hint set: `extension_duration × 70 + source_duration × 10`
|
||||
///
|
||||
/// If you have the source resolution too (480p vs 720p), call
|
||||
/// [`GrokRequestSourceDurationCostCalculator::calculate_cost_in_mills_with_source_duration`]
|
||||
/// instead — that returns the true total.
|
||||
/// directly for an exact figure at the actual rate.
|
||||
fn calculate_cost_in_mills(&self) -> UsdMills {
|
||||
let extension_duration = self.duration.unwrap_or(DEFAULT_EXTENSION_DURATION_SECONDS) as u64;
|
||||
|
||||
// Output inherits source resolution capped at 720p. Use 720p — both the
|
||||
// worst case (most expensive) and the most common output for source
|
||||
// videos generated by /v1/videos/generations at 720p.
|
||||
output_mills_per_second(VideoResolution::SevenTwentyP) * extension_duration
|
||||
let output_mills = output_mills_per_second(VideoResolution::SevenTwentyP) * extension_duration;
|
||||
let input_mills = self
|
||||
.source_video_duration_seconds
|
||||
.map(|secs| INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO * secs as u64)
|
||||
.unwrap_or(0);
|
||||
output_mills + input_mills
|
||||
}
|
||||
}
|
||||
|
||||
impl GrokRequestSourceDurationCostCalculator for VideoExtensionRequest {
|
||||
/// Full cost = output (extension portion, known from request) + input
|
||||
/// (source duration × 10 mills/sec).
|
||||
fn calculate_cost_in_mills_with_source_duration(&self, source_duration_seconds: u32) -> UsdMills {
|
||||
let output_mills = self.calculate_cost_in_mills();
|
||||
/// Full cost:
|
||||
///
|
||||
/// output: output_mills_per_second(source_resolution) × extension_duration
|
||||
/// input: 10 mills/sec × source_duration
|
||||
///
|
||||
/// Output mirrors source resolution (capped at 720p). The `source_resolution`
|
||||
/// parameter should be the source's actual resolution; since [`VideoResolution`]
|
||||
/// has only 480p and 720p, "capping at 720p" is implicit.
|
||||
fn calculate_cost_in_mills_with_source_duration(
|
||||
&self,
|
||||
source_duration_seconds: u32,
|
||||
source_resolution: VideoResolution,
|
||||
) -> UsdMills {
|
||||
let extension_duration = self.duration.unwrap_or(DEFAULT_EXTENSION_DURATION_SECONDS) as u64;
|
||||
let output_mills = output_mills_per_second(source_resolution) * extension_duration;
|
||||
let input_mills = INPUT_MILLS_PER_SECOND_OF_SOURCE_VIDEO * source_duration_seconds as u64;
|
||||
output_mills + input_mills
|
||||
}
|
||||
@@ -70,6 +89,7 @@ mod tests {
|
||||
VideoExtensionRequest {
|
||||
prompt: "test".to_string(),
|
||||
source_video: source,
|
||||
source_video_duration_seconds: None,
|
||||
model,
|
||||
duration,
|
||||
}
|
||||
@@ -167,67 +187,220 @@ mod tests {
|
||||
mod with_source_duration {
|
||||
use super::*;
|
||||
|
||||
// ── 720p source (output 70 mills/sec) ──
|
||||
|
||||
mod source_720p {
|
||||
use super::*;
|
||||
const RES: VideoResolution = VideoResolution::SevenTwentyP;
|
||||
|
||||
#[test]
|
||||
fn five_extension_ten_source() {
|
||||
// output: 70 × 5 = 350 mills; input: 10 × 10 = 100; total 450
|
||||
let req = make_request(Some(5), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10, RES), 450);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10, RES), 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_extension_five_source() {
|
||||
// default ext=6s: output 70 × 6 = 420; input 10 × 5 = 50; total 470
|
||||
let req = make_request(None, url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5, RES), 470);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5, RES), 47);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_extension_zero_source() {
|
||||
// output 70 × 10 = 700; input 0; total 700
|
||||
let req = make_request(Some(10), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0, RES), 700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_scales_linearly() {
|
||||
let req = make_request(Some(5), url_source(), None); // output = 350
|
||||
for secs in 0u32..=30 {
|
||||
let expected = 350 + 10 * secs as u64;
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs, RES), expected, "secs={secs}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_duration_scales_linearly() {
|
||||
// Fix source at 10s (input portion = 100 mills).
|
||||
for ext in 1u32..=10 {
|
||||
let req = make_request(Some(ext), url_source(), None);
|
||||
let expected = 70 * ext as u64 + 100;
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10, RES), expected, "ext={ext}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 480p source (output 50 mills/sec) ──
|
||||
|
||||
mod source_480p {
|
||||
use super::*;
|
||||
const RES: VideoResolution = VideoResolution::FourEightyP;
|
||||
|
||||
#[test]
|
||||
fn five_extension_ten_source() {
|
||||
// output: 50 × 5 = 250 mills; input: 10 × 10 = 100; total 350
|
||||
let req = make_request(Some(5), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10, RES), 350);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10, RES), 35);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_extension_five_source() {
|
||||
// default ext=6s: output 50 × 6 = 300; input 10 × 5 = 50; total 350
|
||||
let req = make_request(None, url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5, RES), 350);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5, RES), 35);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_extension_zero_source() {
|
||||
// output 50 × 10 = 500; input 0; total 500
|
||||
let req = make_request(Some(10), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0, RES), 500);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_scales_linearly() {
|
||||
let req = make_request(Some(5), url_source(), None); // output = 250
|
||||
for secs in 0u32..=30 {
|
||||
let expected = 250 + 10 * secs as u64;
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs, RES), expected, "secs={secs}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_duration_scales_linearly() {
|
||||
// Fix source at 10s (input portion = 100 mills).
|
||||
for ext in 1u32..=10 {
|
||||
let req = make_request(Some(ext), url_source(), None);
|
||||
let expected = 50 * ext as u64 + 100;
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10, RES), expected, "ext={ext}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-resolution comparisons ──
|
||||
|
||||
#[test]
|
||||
fn five_extension_ten_source() {
|
||||
// output: 70 × 5 = 350 mills; input: 10 × 10 = 100 mills; total: 450
|
||||
fn higher_resolution_costs_more_for_fixed_extension_and_source() {
|
||||
let req = make_request(Some(5), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10), 450);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(10), 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_extension_5_source() {
|
||||
// default ext=6s: output 70 × 6 = 420; input 10 × 5 = 50; total 470
|
||||
let req = make_request(None, url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(5), 470);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(5), 47);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_extension_zero_source() {
|
||||
// output 70 × 10 = 700; input 10 × 0 = 0; total 700
|
||||
let req = make_request(Some(10), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(0), 700);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_scales_linearly() {
|
||||
let req = make_request(Some(5), url_source(), None); // output = 350
|
||||
for secs in 0u32..=30 {
|
||||
let expected = 350 + 10 * secs as u64;
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(secs), expected, "secs={secs}");
|
||||
for source_secs in 0u32..=15 {
|
||||
let lo = req.calculate_cost_in_mills_with_source_duration(source_secs, VideoResolution::FourEightyP);
|
||||
let hi = req.calculate_cost_in_mills_with_source_duration(source_secs, VideoResolution::SevenTwentyP);
|
||||
assert!(lo < hi,
|
||||
"source_secs={source_secs}: 480p ({lo}) should be < 720p ({hi})");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_duration_scales_linearly() {
|
||||
// Fix source at 10s (input portion = 100 mills).
|
||||
for ext in 1u32..=10 {
|
||||
fn matrix_of_extension_source_resolution() {
|
||||
// (ext_duration, source_duration, source_resolution, expected_mills)
|
||||
let cases: &[(u32, u32, VideoResolution, u64)] = &[
|
||||
// 480p
|
||||
( 1, 0, VideoResolution::FourEightyP, 50), // 50×1 + 0
|
||||
( 1, 5, VideoResolution::FourEightyP, 100), // 50×1 + 50
|
||||
( 5, 5, VideoResolution::FourEightyP, 300), // 50×5 + 50
|
||||
(10, 5, VideoResolution::FourEightyP, 550), // 50×10 + 50
|
||||
(10, 30, VideoResolution::FourEightyP, 800), // 50×10 + 300
|
||||
// 720p
|
||||
( 1, 0, VideoResolution::SevenTwentyP, 70), // 70×1 + 0
|
||||
( 1, 5, VideoResolution::SevenTwentyP, 120), // 70×1 + 50
|
||||
( 5, 5, VideoResolution::SevenTwentyP, 400), // 70×5 + 50
|
||||
(10, 5, VideoResolution::SevenTwentyP, 750), // 70×10 + 50
|
||||
(10, 30, VideoResolution::SevenTwentyP, 1000), // 70×10 + 300
|
||||
];
|
||||
for &(ext, source, res, expected) in cases {
|
||||
let req = make_request(Some(ext), url_source(), None);
|
||||
let expected = 70 * ext as u64 + 100;
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(10), expected, "ext={ext}");
|
||||
assert_eq!(
|
||||
req.calculate_cost_in_mills_with_source_duration(source, res),
|
||||
expected,
|
||||
"ext={ext} source={source} res={res:?}",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_method_equals_output_only_plus_input() {
|
||||
// The source-duration variant should always equal the base + input portion.
|
||||
let req = make_request(Some(7), url_source(), None);
|
||||
let base = req.calculate_cost_in_mills();
|
||||
for secs in 0u32..=20 {
|
||||
let full = req.calculate_cost_in_mills_with_source_duration(secs);
|
||||
assert_eq!(full, base + 10 * secs as u64, "secs={secs}");
|
||||
fn cents_uses_ceiling_division() {
|
||||
// ext=1s @ 480p: output 50; input 10×1=10; total 60 mills = 6¢
|
||||
let req = make_request(Some(1), url_source(), None);
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1, VideoResolution::FourEightyP), 60);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1, VideoResolution::FourEightyP), 6);
|
||||
// ext=1s @ 720p: output 70; input 10; total 80 mills = 8¢
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1, VideoResolution::SevenTwentyP), 80);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1, VideoResolution::SevenTwentyP), 8);
|
||||
}
|
||||
}
|
||||
|
||||
// ── source_video_duration_seconds hint ──
|
||||
|
||||
mod duration_hint {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn unset_hint_returns_output_only() {
|
||||
// ext=5s @ 720p, no hint → just output portion: 70 × 5 = 350
|
||||
let req = make_request(Some(5), url_source(), None);
|
||||
assert_eq!(req.source_video_duration_seconds, None);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_hint_adds_input_portion() {
|
||||
// ext=5s @ 720p, source=10s → output 350 + input 100 = 450
|
||||
let mut req = make_request(Some(5), url_source(), None);
|
||||
req.source_video_duration_seconds = Some(10);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 450);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 45);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_zero_is_treated_as_zero_input() {
|
||||
// ext=5s @ 720p, source=0 → output 350 + 0 = 350
|
||||
let mut req = make_request(Some(5), url_source(), None);
|
||||
req.source_video_duration_seconds = Some(0);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 350);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_extension_with_hint() {
|
||||
// ext default=6s @ 720p (output 420), source=5s (input 50) → 470
|
||||
let mut req = make_request(None, url_source(), None);
|
||||
req.source_video_duration_seconds = Some(5);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 470);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 47);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hint_at_720p_matches_explicit_source_duration_method() {
|
||||
// The base impl assumes 720p output. The explicit source-duration
|
||||
// method must agree at 720p for every hint value.
|
||||
for ext in 1u32..=10 {
|
||||
for source_secs in 0u32..=20 {
|
||||
let mut req = make_request(Some(ext), url_source(), None);
|
||||
req.source_video_duration_seconds = Some(source_secs);
|
||||
assert_eq!(
|
||||
req.calculate_cost_in_mills(),
|
||||
req.calculate_cost_in_mills_with_source_duration(source_secs, VideoResolution::SevenTwentyP),
|
||||
"ext={ext} source_secs={source_secs}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cents_is_ceil_of_mills_over_ten() {
|
||||
// 75 mills (ext=1s, source=1/2s rounding edge case)
|
||||
// Make sure ceiling division behaves right.
|
||||
let req = make_request(Some(1), url_source(), None); // output = 70 mills
|
||||
// input 10 × 1 = 10; total = 80 mills = 8¢
|
||||
assert_eq!(req.calculate_cost_in_mills_with_source_duration(1), 80);
|
||||
assert_eq!(req.calculate_cost_in_cents_with_source_duration(1), 8);
|
||||
fn hint_scales_linearly() {
|
||||
// Fix ext=5s (output 350); vary hint from 0..30.
|
||||
for source_secs in 0u32..=30 {
|
||||
let mut req = make_request(Some(5), url_source(), None);
|
||||
req.source_video_duration_seconds = Some(source_secs);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 350 + 10 * source_secs as u64, "source_secs={source_secs}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -31,6 +31,18 @@ pub struct VideoExtensionRequest {
|
||||
/// Source video to extend.
|
||||
pub source_video: VideoExtensionSource,
|
||||
|
||||
/// **Cost-estimation hint — NOT sent to xAI, has zero effect on the
|
||||
/// actual HTTP request.** Duration in whole seconds of the video at
|
||||
/// [`source_video`](Self::source_video). When set, the
|
||||
/// [`crate::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator`]
|
||||
/// impl includes the input-side billing (10 mills/sec × source duration)
|
||||
/// in the estimate; when `None`, the impl returns the output portion
|
||||
/// only (extension duration × output rate). Populate this field if you
|
||||
/// already know the source's runtime (e.g. from ffprobe or an upstream
|
||||
/// generate call) so internal cost tracking reflects the true total.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub source_video_duration_seconds: Option<u32>,
|
||||
|
||||
/// Model identifier. Defaults to [`VideoModel::GrokImagineVideo`] when `None`.
|
||||
/// Use [`VideoModel::Custom`] for identifiers not yet listed in the enum.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -170,6 +182,7 @@ mod tests {
|
||||
request: VideoExtensionRequest {
|
||||
prompt: "p".to_string(),
|
||||
source_video: VideoExtensionSource::Url("u".to_string()),
|
||||
source_video_duration_seconds: None,
|
||||
model: None,
|
||||
duration: Some(5),
|
||||
},
|
||||
@@ -178,6 +191,35 @@ mod tests {
|
||||
assert!(!json.contains("secret_must_not_leak"));
|
||||
assert!(json.contains("\"duration\":5"));
|
||||
assert!(json.contains("\"source_video\":{\"Url\":\"u\"}"));
|
||||
// Hint omitted when None.
|
||||
assert!(!json.contains("source_video_duration_seconds"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_hint_serializes_when_set() {
|
||||
let req = VideoExtensionRequest {
|
||||
prompt: "p".to_string(),
|
||||
source_video: VideoExtensionSource::Url("u".to_string()),
|
||||
source_video_duration_seconds: Some(7),
|
||||
model: None,
|
||||
duration: Some(5),
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
assert!(json.contains("\"source_video_duration_seconds\":7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_duration_hint_is_not_in_wire_body() {
|
||||
// The internal wire body (what actually gets POSTed to xAI) doesn't
|
||||
// carry the hint — it's intentionally absent from VideoExtensionRequestBody.
|
||||
let body = VideoExtensionRequestBody {
|
||||
prompt: "p".to_string(),
|
||||
video: VideoExtensionSourceRef { url: Some("u".to_string()), file_id: None },
|
||||
model: None,
|
||||
duration: Some(5),
|
||||
};
|
||||
let json = serde_json::to_string(&body).unwrap();
|
||||
assert!(!json.contains("source_video_duration_seconds"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -203,6 +245,7 @@ mod tests {
|
||||
request: VideoExtensionRequest {
|
||||
prompt: "Continue the walk down the same street".to_string(),
|
||||
source_video: VideoExtensionSource::Url(ANGRY_SHIBA_VIDEO_URL.to_string()),
|
||||
source_video_duration_seconds: None,
|
||||
model: None,
|
||||
duration: Some(5),
|
||||
},
|
||||
|
||||
@@ -106,11 +106,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn fully_default_is_8s_480p() {
|
||||
// 50 × 8 = 400 mills = 40¢
|
||||
fn fully_default_is_8s_720p() {
|
||||
// Default duration 8s, default resolution 720p (per our defaults in cost.rs).
|
||||
// 70 × 8 = 560 mills = 56¢
|
||||
let req = make_request(None, None, None, None);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 400);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 40);
|
||||
assert_eq!(req.calculate_cost_in_mills(), 560);
|
||||
assert_eq!(req.calculate_cost_in_cents(), 56);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -207,9 +208,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_resolution_is_480p() {
|
||||
fn default_resolution_is_720p() {
|
||||
let r1 = make_request(Some(5), None, None, None);
|
||||
let r2 = make_request(Some(5), Some(VideoResolution::FourEightyP), None, None);
|
||||
let r2 = make_request(Some(5), Some(VideoResolution::SevenTwentyP), None, None);
|
||||
assert_eq!(r1.calculate_cost_in_mills(), r2.calculate_cost_in_mills());
|
||||
}
|
||||
}
|
||||
|
||||
+34
-14
@@ -1,31 +1,51 @@
|
||||
//! Source-duration-aware cost calculator for video endpoints that
|
||||
//! reference an existing video (whose duration is NOT in the request body).
|
||||
//! Source-video-aware cost calculator for video endpoints that reference
|
||||
//! an existing video whose duration AND resolution are NOT in the request
|
||||
//! body.
|
||||
//!
|
||||
//! Implemented by:
|
||||
//! - [`crate::api::requests::videos::video_edit::video_edit::VideoEditRequest`]
|
||||
//! - [`crate::api::requests::videos::video_extension::video_extension::VideoExtensionRequest`]
|
||||
//!
|
||||
//! Callers who have the source video's duration (from ffprobe, an upstream
|
||||
//! Callers who have the source video's properties (from ffprobe, an upstream
|
||||
//! generate request that produced it, or similar) can use this to get a
|
||||
//! true total cost. Callers who don't can fall back to
|
||||
//! [`crate::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator`]
|
||||
//! — `video_edit` returns 0 there; `video_extension` returns the output
|
||||
//! portion only.
|
||||
//! true total cost.
|
||||
|
||||
use crate::api::traits::grok_request_cost_calculator_trait::{UsdCents, UsdMills};
|
||||
use crate::api::types::video_types::video_resolution::VideoResolution;
|
||||
|
||||
/// Companion to `GrokRequestCostCalculator` for requests whose true cost
|
||||
/// depends on a source video duration that isn't part of the request body.
|
||||
/// depends on a source video's duration and resolution — neither of which
|
||||
/// is part of the request body.
|
||||
///
|
||||
/// xAI bills:
|
||||
/// - source video as INPUT at 10 mills/sec (rate is independent of resolution)
|
||||
/// - rendered OUTPUT at `output_mills_per_second(resolution)`, where
|
||||
/// `resolution` mirrors the source capped at 720p
|
||||
pub trait GrokRequestSourceDurationCostCalculator {
|
||||
/// Estimated total cost rounded up to the nearest whole cent.
|
||||
fn calculate_cost_in_cents_with_source_duration(&self, source_duration_seconds: u32) -> UsdCents {
|
||||
self.calculate_cost_in_mills_with_source_duration(source_duration_seconds).div_ceil(10)
|
||||
fn calculate_cost_in_cents_with_source_duration(
|
||||
&self,
|
||||
source_duration_seconds: u32,
|
||||
source_resolution: VideoResolution,
|
||||
) -> UsdCents {
|
||||
self
|
||||
.calculate_cost_in_mills_with_source_duration(source_duration_seconds, source_resolution)
|
||||
.div_ceil(10)
|
||||
}
|
||||
|
||||
/// Estimated total cost in mills. Implementations should override this.
|
||||
///
|
||||
/// `source_duration_seconds` is the runtime of the source video at the
|
||||
/// `source_video` field's URL/file_id, in whole seconds. xAI bills source
|
||||
/// video as input at 10 mills/sec regardless of resolution.
|
||||
fn calculate_cost_in_mills_with_source_duration(&self, source_duration_seconds: u32) -> UsdMills;
|
||||
/// `source_duration_seconds`: runtime of the source video in whole
|
||||
/// seconds. xAI bills this at 10 mills/sec regardless of resolution.
|
||||
///
|
||||
/// `source_resolution`: resolution of the source video. xAI's edit and
|
||||
/// extension endpoints produce output at this resolution (capped at
|
||||
/// 720p), so the caller should pass the *effective* output resolution —
|
||||
/// i.e. `min(source_resolution, 720p)`. Since [`VideoResolution`] only
|
||||
/// has `480p` and `720p` variants, "capping at 720p" is automatic.
|
||||
fn calculate_cost_in_mills_with_source_duration(
|
||||
&self,
|
||||
source_duration_seconds: u32,
|
||||
source_resolution: VideoResolution,
|
||||
) -> UsdMills;
|
||||
}
|
||||
Reference in new issue
Block a user