From bcc27875702bbe7aa56d0b2e181a85534229c846 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Tue, 9 Jun 2026 22:47:24 -0400 Subject: [PATCH] no longer page on kinovi 400s --- .../common/map_seedance2pro_web_errors.rs | 85 ++++++++++++++++++ .../endpoints/generate/common/mod.rs | 1 + ...ce_2p0_multi_function_video_gen_handler.rs | 43 +++++---- .../image/pipeline_v2/run_pipeline_v2.rs | 5 +- .../video/pipeline_v2/run_pipeline_v2.rs | 5 +- .../map_seedance2pro_router_error.rs | 90 +++++++++++++++++++ .../endpoints/omni_gen/shared_utils/mod.rs | 3 +- 7 files changed, 212 insertions(+), 20 deletions(-) create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/map_seedance2pro_web_errors.rs create mode 100644 crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/map_seedance2pro_router_error.rs diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/map_seedance2pro_web_errors.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/map_seedance2pro_web_errors.rs new file mode 100644 index 0000000000..e776ba93de --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/map_seedance2pro_web_errors.rs @@ -0,0 +1,85 @@ +//! Maps Kinovi (Seedance2Pro) client errors to web errors. +//! +//! Known bad requests (content violations, over-long prompts, too many urls) +//! become 400s with a user-facing message. Everything else is unanticipated +//! and stays a 500. + +use seedance2pro_client::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError; +use seedance2pro_client::error::seedance2pro_error::Seedance2ProError; + +use crate::http_server::common_responses::common_web_error::CommonWebError; + +/// Map a raw Kinovi client error to a web error. +pub fn map_seedance2pro_error_to_web_error(error: Seedance2ProError) -> CommonWebError { + match error { + Seedance2ProError::ApiBadRequest(bad_request) => { + CommonWebError::BadInputWithSimpleMessage(user_facing_bad_request_message(&bad_request)) + } + other => CommonWebError::from_error(other), + } +} + +fn user_facing_bad_request_message(error: &Seedance2ProBadRequestApiError) -> String { + match error { + Seedance2ProBadRequestApiError::VideoGenerationViolation { .. } => { + "The generation request was flagged as a content violation. \ + Please adjust your prompt or input media and try again.".to_string() + } + Seedance2ProBadRequestApiError::PromptIsTooLong { .. } => { + "The prompt is too long. Please shorten it and try again.".to_string() + } + Seedance2ProBadRequestApiError::TooManyUrls { .. } => { + "Too many input files were attached. \ + Please remove some and try again.".to_string() + } + } +} + +#[cfg(test)] +mod tests { + use actix_web::ResponseError; + use seedance2pro_client::error::seedance2pro_specific_api_error::Seedance2ProSpecificApiError; + + use super::*; + + #[test] + fn bad_requests_become_400s_with_user_facing_messages() { + let cases = [ + ( + Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: "{}".to_string() }, + "content violation", + ), + ( + Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: "{}".to_string() }, + "prompt is too long", + ), + ( + Seedance2ProBadRequestApiError::TooManyUrls { raw_body: "{}".to_string() }, + "Too many input files", + ), + ]; + + for (bad_request, expected_message_fragment) in cases { + let error = map_seedance2pro_error_to_web_error( + Seedance2ProError::ApiBadRequest(bad_request)); + + assert_eq!(error.status_code().as_u16(), 400); + match error { + CommonWebError::BadInputWithSimpleMessage(message) => { + assert!( + message.contains(expected_message_fragment), + "message {:?} should contain {:?}", message, expected_message_fragment); + } + other => panic!("Expected BadInputWithSimpleMessage, got: {:?}", other), + } + } + } + + #[test] + fn unanticipated_errors_stay_500s() { + let error = map_seedance2pro_error_to_web_error( + Seedance2ProError::ApiSpecific(Seedance2ProSpecificApiError::UnauthorizedSessionExpired)); + + assert_eq!(error.status_code().as_u16(), 500); + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/mod.rs index 6a9b59468d..fc99a874d5 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/common/mod.rs @@ -1,3 +1,4 @@ pub mod job_failure_test; +pub mod map_seedance2pro_web_errors; pub mod payments_error_test; mod test_synthetic_failure_reason; \ No newline at end of file diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs index b5e8dc053b..7dbf99cca6 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/generate/video/multi_function/seedance_2p0_multi_function_video_gen_handler.rs @@ -1,9 +1,10 @@ use std::collections::HashMap; use std::sync::Arc; - +use actix_http::StatusCode; use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::endpoint_helpers::refund_wallet_after_api_failure::refund_wallet_after_api_failure; +use crate::http_server::endpoints::generate::common::map_seedance2pro_web_errors::map_seedance2pro_error_to_web_error; use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test; use crate::http_server::session::lookup::user_session_feature_flags::UserSessionFeatureFlags; use crate::http_server::validations::validate_idempotency_token_format::validate_idempotency_token_format; @@ -12,7 +13,7 @@ use crate::state::server_state::ServerState; use crate::util::http_download_url_to_bytes::http_download_url_to_bytes; use crate::util::lookup::lookup_media_file_urls_as_map::lookup_media_file_urls_as_map; use actix_web::web::Json; -use actix_web::{web, HttpRequest}; +use actix_web::{web, HttpRequest, ResponseError}; use artcraft_api_defs::generate::video::multi_function::seedance_2p0_multi_function_video_gen::{ Seedance2p0AspectRatio, Seedance2p0BatchCount, Seedance2p0MultiFunctionVideoGenRequest, Seedance2p0MultiFunctionVideoGenResponse, Seedance2p0OutputResolution, @@ -234,13 +235,19 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( } Err(err) => { warn!("Regular session fallback also failed for user {:?}: {:?}", user_token, err); - let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into()) - .set_title("Seedance 2.0 generation failed (whitelist + fallback)".to_string()) - .set_urgency(Some(NotificationUrgency::High)) - .build(); - if let Err(page_err) = server_state.pager.enqueue_page(notification) { - warn!("Failed to enqueue pager alert: {:?}", page_err); + + // Client-fault errors (eg. content violations, over-long prompts) + // surface to the user as 400s and shouldn't page. + if err.status_code() != StatusCode::BAD_REQUEST { + let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into()) + .set_title("Seedance 2.0 generation failed (whitelist + fallback)".to_string()) + .set_urgency(Some(NotificationUrgency::High)) + .build(); + if let Err(page_err) = server_state.pager.enqueue_page(notification) { + warn!("Failed to enqueue pager alert: {:?}", page_err); + } } + refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?; return Err(err); } @@ -269,13 +276,19 @@ pub async fn seedance_2p0_multi_function_video_gen_handler( Ok(result) => result, Err(err) => { warn!("Error calling seedance2pro generate_video: {:?}", err); - let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into()) - .set_title("Seedance 2.0 generation failed".to_string()) - .set_urgency(Some(NotificationUrgency::High)) - .build(); - if let Err(page_err) = server_state.pager.enqueue_page(notification) { - warn!("Failed to enqueue pager alert: {:?}", page_err); + + // Client-fault errors (eg. content violations, over-long prompts) + // surface to the user as 400s and shouldn't page. + if err.status_code() != StatusCode::BAD_REQUEST { + let notification = NotificationDetailsBuilder::from_boxed_error(err.clone().into()) + .set_title("Seedance 2.0 generation failed".to_string()) + .set_urgency(Some(NotificationUrgency::High)) + .build(); + if let Err(page_err) = server_state.pager.enqueue_page(notification) { + warn!("Failed to enqueue pager alert: {:?}", page_err); + } } + refund_wallet_after_api_failure(&deduction_result.ledger_entry_token, &mut mysql_connection).await?; return Err(err); } @@ -613,7 +626,7 @@ async fn upload_and_generate( let gen_response = generate_video(video_gen_args).await .map_err(|err| { warn!("Error calling seedance2pro generate_video: {:?}", err); - CommonWebError::from_error(err) + map_seedance2pro_error_to_web_error(err) })?; Ok(SeedanceGenerationResult { diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/pipeline_v2/run_pipeline_v2.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/pipeline_v2/run_pipeline_v2.rs index 22552a640a..bb1ad6dd77 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/pipeline_v2/run_pipeline_v2.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/image/pipeline_v2/run_pipeline_v2.rs @@ -18,6 +18,7 @@ use tokens::tokens::users::UserToken; use crate::billing::wallets::attempt_wallet_deduction::attempt_wallet_deduction_else_common_web_error; use crate::http_server::common_responses::common_web_error::CommonWebError; use crate::http_server::endpoints::omni_gen::generate::image::pipeline_result::ImagePipelineResult; +use crate::http_server::endpoints::omni_gen::shared_utils::map_seedance2pro_router_error::map_router_error_to_web_error; use crate::state::server_state::ServerState; use crate::util::lookup::lookup_media_files_as_cdn_url_list_and_map::MediaFilesAsCdnUrlListAndMap; @@ -155,7 +156,7 @@ async fn finalize_and_generate( .await .map_err(|err| { warn!("Failed to finalize image v2 draft: {:?}", err); - CommonWebError::from_error(err) + map_router_error_to_web_error(err) })? } }; @@ -164,7 +165,7 @@ async fn finalize_and_generate( .await .map_err(|err| { warn!("v2 image generation failed: {:?}", err); - CommonWebError::from_error(err) + map_router_error_to_web_error(err) }) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs index 987270ca93..a772cba319 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/generate/video/pipeline_v2/run_pipeline_v2.rs @@ -19,6 +19,7 @@ use crate::http_server::endpoints::omni_gen::generate::video::helpers::build_rou use crate::http_server::endpoints::omni_gen::generate::video::helpers::pipeline_result::PipelineResult; use crate::http_server::endpoints::omni_gen::generate::video::helpers::resolve_media_tokens_to_urls::resolve_media_tokens_to_urls; use crate::http_server::endpoints::omni_gen::generate::video::kinovi_account::KinoviAccount; +use crate::http_server::endpoints::omni_gen::shared_utils::map_seedance2pro_router_error::map_router_error_to_web_error; use crate::state::server_state::ServerState; pub struct RunPipelineV2Args<'a> { @@ -170,7 +171,7 @@ async fn upload_and_generate( .await .map_err(|err| { warn!("Failed to finalize v2 draft: {:?}", err); - CommonWebError::from_error(err) + map_router_error_to_web_error(err) })? } }; @@ -179,6 +180,6 @@ async fn upload_and_generate( .await .map_err(|err| { warn!("v2 video generation failed: {:?}", err); - CommonWebError::from_error(err) + map_router_error_to_web_error(err) }) } diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/map_seedance2pro_router_error.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/map_seedance2pro_router_error.rs new file mode 100644 index 0000000000..6815fa3869 --- /dev/null +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/map_seedance2pro_router_error.rs @@ -0,0 +1,90 @@ +//! Maps artcraft router errors to web errors for the omni_gen pipelines. +//! +//! Known Kinovi (Seedance2Pro) bad requests (content violations, over-long +//! prompts, too many urls) become 400s with a user-facing message. Everything +//! else is unanticipated and stays a 500. + +use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError; +use artcraft_router::errors::provider_error::ProviderError; +use seedance2pro_client::error::seedance2pro_bad_request_api_error::Seedance2ProBadRequestApiError; +use seedance2pro_client::error::seedance2pro_error::Seedance2ProError; + +use crate::http_server::common_responses::common_web_error::CommonWebError; + +/// Map an artcraft router error to a web error, unwrapping Kinovi bad requests. +pub fn map_router_error_to_web_error(error: ArtcraftRouterError) -> CommonWebError { + match error { + ArtcraftRouterError::Provider( + ProviderError::Seedance2Pro(Seedance2ProError::ApiBadRequest(bad_request)) + ) => { + CommonWebError::BadInputWithSimpleMessage(user_facing_bad_request_message(&bad_request)) + } + other => CommonWebError::from_error(other), + } +} + +fn user_facing_bad_request_message(error: &Seedance2ProBadRequestApiError) -> String { + match error { + Seedance2ProBadRequestApiError::VideoGenerationViolation { .. } => { + "The generation request was flagged as a content violation. \ + Please adjust your prompt or input media and try again.".to_string() + } + Seedance2ProBadRequestApiError::PromptIsTooLong { .. } => { + "The prompt is too long. Please shorten it below 10,000 characters and try again.".to_string() + } + Seedance2ProBadRequestApiError::TooManyUrls { .. } => { + "Too many input files were attached. \ + Please remove some and try again.".to_string() + } + } +} + +#[cfg(test)] +mod tests { + use actix_web::ResponseError; + + use super::*; + + #[test] + fn kinovi_bad_requests_become_400s_with_user_facing_messages() { + let cases = [ + ( + Seedance2ProBadRequestApiError::VideoGenerationViolation { raw_body: "{}".to_string() }, + "content violation", + ), + ( + Seedance2ProBadRequestApiError::PromptIsTooLong { raw_body: "{}".to_string() }, + "prompt is too long", + ), + ( + Seedance2ProBadRequestApiError::TooManyUrls { raw_body: "{}".to_string() }, + "Too many input files", + ), + ]; + + for (bad_request, expected_message_fragment) in cases { + let error = map_router_error_to_web_error( + ArtcraftRouterError::Provider( + ProviderError::Seedance2Pro( + Seedance2ProError::ApiBadRequest(bad_request)))); + + assert_eq!(error.status_code().as_u16(), 400); + match error { + CommonWebError::BadInputWithSimpleMessage(message) => { + assert!( + message.contains(expected_message_fragment), + "message {:?} should contain {:?}", message, expected_message_fragment); + } + other => panic!("Expected BadInputWithSimpleMessage, got: {:?}", other), + } + } + } + + #[test] + fn other_router_errors_stay_500s() { + let error = map_router_error_to_web_error( + ArtcraftRouterError::UnsupportedModel("some_model".to_string())); + + assert_eq!(error.status_code().as_u16(), 500); + } +} diff --git a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/mod.rs b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/mod.rs index 8e5d9b9132..cf024715fc 100644 --- a/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/mod.rs +++ b/crates/service/web/storyteller_web/src/http_server/endpoints/omni_gen/shared_utils/mod.rs @@ -1 +1,2 @@ -pub mod video; \ No newline at end of file +pub mod map_seedance2pro_router_error; +pub mod video;