don't send default resolution anymore

This commit is contained in:
Hanashi
2026-08-13 01:28:39 -04:00
parent 5cacd89ab8
commit edc9dbb8ed
15 changed files with 106 additions and 79 deletions
@@ -100,7 +100,10 @@ fn plan_output_resolution(
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviHappyHorse1p0OutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Unset defaults to explicit 720p. Cost estimation prices unset as 720p,
// so the request must pin 720p too — never leave the resolution up to the
// provider's server-side default, or billing and supplier cost can diverge.
None => Ok(Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP)),
Some(RouterResolution::SevenTwentyP) => Ok(Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP)),
Some(RouterResolution::TenEightyP) => Ok(Some(KinoviHappyHorse1p0OutputResolution::TenEightyP)),
@@ -332,10 +335,10 @@ mod tests {
}
#[test]
fn resolution_none() {
fn resolution_none_defaults_to_720p() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..happy_horse_builder() };
let draft = unwrap_draft(build_kinovi_happy_horse_1p0(builder));
assert!(draft.resolution.is_none());
assert!(matches!(draft.resolution, Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP)));
}
#[test]
@@ -117,7 +117,10 @@ fn plan_output_resolution(
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Unset defaults to explicit 720p. Cost estimation prices unset as 720p,
// so the request must pin 720p too — never leave the resolution up to the
// provider's server-side default, or billing and supplier cost can diverge.
None => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// Direct mappings
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
@@ -393,10 +396,10 @@ mod tests {
}
#[test]
fn resolution_none() {
fn resolution_none_defaults_to_720p() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..kinovi_web_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0(builder));
assert!(draft.resolution.is_none());
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
@@ -435,7 +435,8 @@ mod tests {
fn from_draft_720p_default() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0CostState::from_draft(&draft);
assert!(cost.resolution.is_none()); // None defaults to 720p pricing
// Unset resolution is pinned to explicit 720p at build time.
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
assert!(!cost.has_video_reference);
@@ -118,7 +118,10 @@ fn plan_output_resolution(
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Unset defaults to explicit 720p. Cost estimation prices unset as 720p,
// so the request must pin 720p too — never leave the resolution up to the
// provider's server-side default, or billing and supplier cost can diverge.
None => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// Direct mappings
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
@@ -421,10 +424,10 @@ mod tests {
}
#[test]
fn resolution_none() {
fn resolution_none_defaults_to_720p() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..kinovi_web_fast_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_fast(builder));
assert!(draft.resolution.is_none());
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
#[test]
@@ -266,7 +266,8 @@ mod tests {
fn from_draft_720p_default() {
let draft = make_draft(5, 1, None, false);
let cost = KinoviSeedance2p0FastCostState::from_draft(&draft);
assert!(cost.resolution.is_none());
// Unset resolution is pinned to explicit 720p at build time.
assert!(matches!(cost.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
assert_eq!(cost.duration_seconds, 5);
assert!(matches!(cost.batch_count, Some(KinoviBatchCount::One)));
assert!(!cost.has_video_reference);
@@ -116,7 +116,10 @@ fn plan_output_resolution(
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Unset defaults to explicit 720p. Cost estimation prices unset as 720p,
// so the request must pin 720p too — never leave the resolution up to the
// provider's server-side default, or billing and supplier cost can diverge.
None => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// Direct mappings
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
@@ -429,10 +432,10 @@ mod tests {
}
#[test]
fn resolution_none() {
fn resolution_none_defaults_to_720p() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..mini_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p0_mini(builder));
assert!(draft.resolution.is_none());
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
}
@@ -151,7 +151,10 @@ fn plan_output_resolution(
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Unset defaults to explicit 720p. Cost estimation prices unset as 720p,
// so the request must pin 720p too — never leave the resolution up to the
// provider's server-side default, or billing and supplier cost can diverge.
None => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// Direct mappings
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
@@ -134,7 +134,10 @@ fn plan_output_resolution(
strategy: RequestMismatchMitigationStrategy,
) -> Result<Option<KinoviOutputResolution>, ArtcraftRouterError> {
match resolution {
None => Ok(None),
// Unset defaults to explicit 720p. Cost estimation prices unset as 720p,
// so the request must pin 720p too — never leave the resolution up to the
// provider's server-side default, or billing and supplier cost can diverge.
None => Ok(Some(KinoviOutputResolution::SevenTwentyP)),
// Direct mappings
Some(RouterResolution::FourEightyP) => Ok(Some(KinoviOutputResolution::FourEightyP)),
@@ -439,10 +442,10 @@ mod tests {
}
#[test]
fn resolution_none() {
fn resolution_none_defaults_to_720p() {
let builder = GenerateVideoRequestBuilder { resolution: None, ..preview_builder() };
let draft = unwrap_draft(build_kinovi_seedance_2p5_preview(builder));
assert!(draft.resolution.is_none());
assert!(matches!(draft.resolution, Some(KinoviOutputResolution::SevenTwentyP)));
}
}
@@ -119,7 +119,7 @@ pub async fn generate_happy_horse_1p0(
model_type: KinoviModelTypeRaw::HappyHorse1p0,
prompt: req.prompt,
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
output_resolution: req.output_resolution.map(map_output_resolution),
output_resolution: Some(map_output_resolution(req.output_resolution)),
batch_count: map_batch_count(req.batch_count),
duration_seconds: req.duration_seconds,
start_frame_url: req.start_frame_url,
@@ -159,10 +159,12 @@ fn map_aspect_ratio(ar: Option<KinoviHappyHorse1p0AspectRatio>) -> KinoviAspectR
}
}
fn map_output_resolution(res: KinoviHappyHorse1p0OutputResolution) -> KinoviOutputResolutionRaw {
fn map_output_resolution(res: Option<KinoviHappyHorse1p0OutputResolution>) -> KinoviOutputResolutionRaw {
match res {
KinoviHappyHorse1p0OutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
KinoviHappyHorse1p0OutputResolution::TenEightyP => KinoviOutputResolutionRaw::TenEightyP,
// Unset resolves to 720p — MUST stay in lockstep with calculate_costs(),
// which prices None as 720p.
Some(KinoviHappyHorse1p0OutputResolution::SevenTwentyP) | None => KinoviOutputResolutionRaw::SevenTwentyP,
Some(KinoviHappyHorse1p0OutputResolution::TenEightyP) => KinoviOutputResolutionRaw::TenEightyP,
}
}
@@ -183,7 +183,7 @@ pub async fn generate_seedance_2p0(
model_type: KinoviModelTypeRaw::Seedance2Pro,
prompt: req.prompt,
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
output_resolution: req.output_resolution.map(map_output_resolution),
output_resolution: Some(map_output_resolution(req.output_resolution)),
batch_count: map_batch_count(req.batch_count),
duration_seconds: req.duration_seconds,
start_frame_url: req.start_frame_url,
@@ -224,12 +224,16 @@ fn map_aspect_ratio(ar: Option<KinoviSeedance2p0AspectRatio>) -> KinoviAspectRat
}
}
fn map_output_resolution(res: KinoviSeedance2p0OutputResolution) -> KinoviOutputResolutionRaw {
fn map_output_resolution(res: Option<KinoviSeedance2p0OutputResolution>) -> KinoviOutputResolutionRaw {
match res {
KinoviSeedance2p0OutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
KinoviSeedance2p0OutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
KinoviSeedance2p0OutputResolution::TenEightyP => KinoviOutputResolutionRaw::TenEightyP,
KinoviSeedance2p0OutputResolution::FourK => KinoviOutputResolutionRaw::FourK,
Some(KinoviSeedance2p0OutputResolution::FourEightyP) => KinoviOutputResolutionRaw::FourEightyP,
// Unset resolves to 720p — MUST stay in lockstep with calculate_costs(),
// which prices None as 720p. Never leave the resolution to the Kinovi
// server-side default; a silent upstream change would make supplier cost
// diverge from what we billed.
Some(KinoviSeedance2p0OutputResolution::SevenTwentyP) | None => KinoviOutputResolutionRaw::SevenTwentyP,
Some(KinoviSeedance2p0OutputResolution::TenEightyP) => KinoviOutputResolutionRaw::TenEightyP,
Some(KinoviSeedance2p0OutputResolution::FourK) => KinoviOutputResolutionRaw::FourK,
}
}
@@ -173,7 +173,7 @@ pub async fn generate_seedance_2p0_fast(
model_type: KinoviModelTypeRaw::Seedance2Fast,
prompt: req.prompt,
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
output_resolution: req.output_resolution.map(map_output_resolution),
output_resolution: Some(map_output_resolution(req.output_resolution)),
batch_count: map_batch_count(req.batch_count),
duration_seconds: req.duration_seconds,
start_frame_url: req.start_frame_url,
@@ -214,10 +214,12 @@ fn map_aspect_ratio(ar: Option<KinoviSeedance2p0FastAspectRatio>) -> KinoviAspec
}
}
fn map_output_resolution(res: KinoviSeedance2p0FastOutputResolution) -> KinoviOutputResolutionRaw {
fn map_output_resolution(res: Option<KinoviSeedance2p0FastOutputResolution>) -> KinoviOutputResolutionRaw {
match res {
KinoviSeedance2p0FastOutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
KinoviSeedance2p0FastOutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
Some(KinoviSeedance2p0FastOutputResolution::FourEightyP) => KinoviOutputResolutionRaw::FourEightyP,
// Unset resolves to 720p — MUST stay in lockstep with calculate_costs(),
// which prices None as 720p.
Some(KinoviSeedance2p0FastOutputResolution::SevenTwentyP) | None => KinoviOutputResolutionRaw::SevenTwentyP,
}
}
@@ -179,7 +179,7 @@ fn to_raw_request(req: GenerateSeedance2p0MiniRequest) -> WorkflowRunTaskRequest
model_type: KinoviModelTypeRaw::Seedance2Mini,
prompt: req.prompt,
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
output_resolution: req.output_resolution.map(map_output_resolution),
output_resolution: Some(map_output_resolution(req.output_resolution)),
batch_count: map_batch_count(req.batch_count),
duration_seconds: req.duration_seconds,
start_frame_url: req.start_frame_url,
@@ -205,10 +205,12 @@ fn map_aspect_ratio(ar: Option<KinoviSeedance2p0MiniAspectRatio>) -> KinoviAspec
}
}
fn map_output_resolution(res: KinoviSeedance2p0MiniOutputResolution) -> KinoviOutputResolutionRaw {
fn map_output_resolution(res: Option<KinoviSeedance2p0MiniOutputResolution>) -> KinoviOutputResolutionRaw {
match res {
KinoviSeedance2p0MiniOutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
KinoviSeedance2p0MiniOutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
Some(KinoviSeedance2p0MiniOutputResolution::FourEightyP) => KinoviOutputResolutionRaw::FourEightyP,
// Unset resolves to 720p — MUST stay in lockstep with calculate_costs(),
// which prices None as 720p.
Some(KinoviSeedance2p0MiniOutputResolution::SevenTwentyP) | None => KinoviOutputResolutionRaw::SevenTwentyP,
}
}
@@ -566,11 +568,11 @@ mod tests {
}
#[test]
fn default_resolution_maps_to_none() {
fn default_resolution_maps_to_720p() {
let mut req = sample();
req.output_resolution = None;
let raw = to_raw_request(req);
assert!(raw.output_resolution.is_none());
assert!(matches!(raw.output_resolution, Some(KinoviOutputResolutionRaw::SevenTwentyP)));
}
#[test]
@@ -234,7 +234,7 @@ fn to_raw_request(req: GenerateSeedance2p5Request) -> WorkflowRunTaskRequest {
model_type: KinoviModelTypeRaw::Seedance2p5,
prompt: req.prompt,
aspect_ratio,
output_resolution: req.output_resolution.map(map_output_resolution),
output_resolution: Some(map_output_resolution(req.output_resolution)),
duration_seconds: req.duration_seconds,
batch_count: KinoviBatchCountRaw::One,
start_frame_url,
@@ -260,10 +260,12 @@ fn map_aspect_ratio(ar: Option<KinoviSeedance2p5AspectRatio>) -> KinoviAspectRat
}
}
fn map_output_resolution(res: KinoviSeedance2p5OutputResolution) -> KinoviOutputResolutionRaw {
fn map_output_resolution(res: Option<KinoviSeedance2p5OutputResolution>) -> KinoviOutputResolutionRaw {
match res {
KinoviSeedance2p5OutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
KinoviSeedance2p5OutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
Some(KinoviSeedance2p5OutputResolution::FourEightyP) => KinoviOutputResolutionRaw::FourEightyP,
// Unset resolves to 720p — MUST stay in lockstep with calculate_costs(),
// which prices None as 720p.
Some(KinoviSeedance2p5OutputResolution::SevenTwentyP) | None => KinoviOutputResolutionRaw::SevenTwentyP,
}
}
@@ -122,7 +122,7 @@ fn to_raw_request(req: GenerateSeedance2p5PreviewRequest) -> WorkflowRunTaskRequ
model_type: KinoviModelTypeRaw::Seedance2p5Preview,
prompt: req.prompt,
aspect_ratio: map_aspect_ratio(req.aspect_ratio),
output_resolution: req.output_resolution.map(map_output_resolution),
output_resolution: Some(map_output_resolution(req.output_resolution)),
duration_seconds: req.duration_seconds,
batch_count: KinoviBatchCountRaw::One,
start_frame_url: None,
@@ -148,10 +148,12 @@ fn map_aspect_ratio(ar: Option<KinoviSeedance2p5PreviewAspectRatio>) -> KinoviAs
}
}
fn map_output_resolution(res: KinoviSeedance2p5PreviewOutputResolution) -> KinoviOutputResolutionRaw {
fn map_output_resolution(res: Option<KinoviSeedance2p5PreviewOutputResolution>) -> KinoviOutputResolutionRaw {
match res {
KinoviSeedance2p5PreviewOutputResolution::FourEightyP => KinoviOutputResolutionRaw::FourEightyP,
KinoviSeedance2p5PreviewOutputResolution::SevenTwentyP => KinoviOutputResolutionRaw::SevenTwentyP,
Some(KinoviSeedance2p5PreviewOutputResolution::FourEightyP) => KinoviOutputResolutionRaw::FourEightyP,
// Unset resolves to 720p — MUST stay in lockstep with calculate_costs(),
// which prices None as 720p.
Some(KinoviSeedance2p5PreviewOutputResolution::SevenTwentyP) | None => KinoviOutputResolutionRaw::SevenTwentyP,
}
}
@@ -139,12 +139,14 @@ impl KinoviAspectRatioRaw {
}
}
/// Output resolution quality. When omitted, defaults to 720p.
/// Output resolution quality. Always sent explicitly on the wire — we never
/// omit the field and rely on the Kinovi server-side default, because the
/// billed price assumes a specific resolution and the two must not diverge.
#[derive(Debug, Clone, Copy)]
pub enum KinoviOutputResolutionRaw {
/// 480p
FourEightyP,
/// 720p (default — omitting the field gives this)
/// 720p (what unset requests resolve to)
SevenTwentyP,
/// 1080p
TenEightyP,
@@ -154,20 +156,8 @@ pub enum KinoviOutputResolutionRaw {
}
impl KinoviOutputResolutionRaw {
/// Returns the API string to send, or None for 720p (the default, which is
/// expressed by omitting the field entirely).
pub fn as_api_str(&self) -> Option<&'static str> {
match self {
Self::FourEightyP => Some("480p"),
Self::SevenTwentyP => None, // Default — omit from request
Self::TenEightyP => Some("1080p"),
Self::FourK => Some("4k"),
}
}
/// The API string including the "720p" default, for models that always
/// send `outputResolution` explicitly (Seedance 2.5 Preview).
pub fn as_explicit_api_str(&self) -> &'static str {
/// The API string to send.
pub fn as_api_str(&self) -> &'static str {
match self {
Self::FourEightyP => "480p",
Self::SevenTwentyP => "720p",
@@ -289,9 +279,10 @@ impl KinoviModelTypeRaw {
matches!(self, Self::Seedance2p5)
}
/// Whether the default field values (`outputResolution: "720p"`,
/// `faceBlurMode: "off"`) are always sent explicitly rather than omitted.
/// The 2.5 family always sends both.
/// Whether the default `faceBlurMode: "off"` is always sent explicitly
/// rather than omitted. The 2.5 family always sends it.
/// (`outputResolution` is always sent explicitly for EVERY model — see
/// `build_batch_request` — so it is no longer governed by this flag.)
fn always_sends_default_fields(&self) -> bool {
matches!(self, Self::Seedance2p5Preview | Self::Seedance2p5)
}
@@ -533,11 +524,13 @@ fn build_batch_request(req: WorkflowRunTaskRequest) -> BatchRequest {
None => None,
};
let output_resolution = if req.model_type.always_sends_default_fields() {
// ALWAYS send the resolution explicitly, defaulting unset to 720p. We used
// to omit the field for 720p (mimicking the Kinovi web UI) and let the
// server default apply, but that let the supplier-side resolution — and
// therefore the supplier cost — float free of what we billed. Pin it.
let output_resolution = {
let resolution = req.output_resolution.unwrap_or(KinoviOutputResolutionRaw::SevenTwentyP);
Some(resolution.as_explicit_api_str())
} else {
req.output_resolution.and_then(|r| r.as_api_str())
Some(resolution.as_api_str())
};
let batch_count_value = req.batch_count.as_u8();
@@ -661,30 +654,30 @@ mod tests {
// ── Output-resolution serialization ──
//
// `outputResolution` is sent only when non-default; 720p (the default) omits
// the field. 4K serializes to the literal "4k".
// `outputResolution` is always sent explicitly; 720p serializes to the
// literal "720p" and 4K to the literal "4k".
mod output_resolution_serialization_tests {
use super::*;
#[test]
fn four_k_maps_to_4k() {
assert_eq!(KinoviOutputResolutionRaw::FourK.as_api_str(), Some("4k"));
assert_eq!(KinoviOutputResolutionRaw::FourK.as_api_str(), "4k");
}
#[test]
fn four_k_serializes_on_the_wire() {
let api_params = base_api_params(KinoviOutputResolutionRaw::FourK.as_api_str());
let api_params = base_api_params(Some(KinoviOutputResolutionRaw::FourK.as_api_str()));
let json = serde_json::to_string(&api_params).unwrap();
assert!(json.contains(r#""outputResolution":"4k""#), "expected outputResolution in {json}");
}
#[test]
fn default_720p_omits_output_resolution() {
assert_eq!(KinoviOutputResolutionRaw::SevenTwentyP.as_api_str(), None);
let api_params = base_api_params(KinoviOutputResolutionRaw::SevenTwentyP.as_api_str());
fn default_720p_serializes_explicitly() {
assert_eq!(KinoviOutputResolutionRaw::SevenTwentyP.as_api_str(), "720p");
let api_params = base_api_params(Some(KinoviOutputResolutionRaw::SevenTwentyP.as_api_str()));
let json = serde_json::to_string(&api_params).unwrap();
assert!(!json.contains("outputResolution"), "expected no outputResolution in {json}");
assert!(json.contains(r#""outputResolution":"720p""#), "expected outputResolution in {json}");
}
fn base_api_params(output_resolution: Option<&'static str>) -> ApiParams {
@@ -760,7 +753,7 @@ mod tests {
}
#[test]
fn mini_720p_omits_output_resolution() {
fn mini_unset_resolution_sends_explicit_720p() {
let body = build_batch_request(mini_request(
KinoviAspectRatioRaw::Landscape16x9,
None,
@@ -768,7 +761,7 @@ mod tests {
));
let json = serde_json::to_string(&body).unwrap();
assert!(json.contains(r#""aspectRatio":"16:9""#), "{json}");
assert!(!json.contains("outputResolution"), "{json}");
assert!(json.contains(r#""outputResolution":"720p""#), "{json}");
}
#[test]