remove from omni api

This commit is contained in:
Hanashi
2026-08-06 17:38:01 -04:00
parent c21463aa1a
commit 55a7aa35b5
7 changed files with 100 additions and 425 deletions
@@ -1,150 +0,0 @@
//! Enqueue flow for first-party Minimax H3 (Turbo / Ultra) video generation.
//!
//! These models run on our own GPU inference, so unlike the provider-backed
//! models there is no external generation call: we bill (Ultra only), write
//! the prompt + job records, and return. A scheduler will pick the pending
//! jobs up later.
use std::convert::TryFrom;
use actix_web::web::Json;
use log::{error, info};
use sqlx::Acquire;
use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_request::OmniApiVideoGenerateRequest;
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::platform_type::PlatformType;
use enums::common::visibility::Visibility;
use mysql_queries::queries::generic_inference::first_party::minimax_h3::insert_generic_inference_job_for_first_party_minimax_h3_with_apriori_job_token::{
insert_generic_inference_job_for_first_party_minimax_h3_with_apriori_job_token,
FirstPartyMinimaxH3Model,
InsertGenericInferenceForFirstPartyMinimaxH3WithAprioriJobTokenArgs,
};
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
use tokens::tokens::users::UserToken;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_api::generate::video::first_party_minimax_h3::minimax_h3_ultra_cost::estimate_minimax_h3_ultra_cost_usd_cents;
use crate::http_server::endpoints::omni_api::generate::video::helpers::bill_wallet::bill_wallet;
use crate::http_server::endpoints::omni_api::generate::video::helpers::write_prompt_records::{
write_prompt_records, WritePromptRecordsArgs,
};
pub struct EnqueueFirstPartyMinimaxH3JobArgs<'a> {
pub minimax_model: FirstPartyMinimaxH3Model,
pub request: &'a OmniApiVideoGenerateRequest,
pub user_token: &'a UserToken,
pub maybe_prompt_model_type: Option<CommonModelType>,
pub idempotency_token: &'a str,
pub ip_address: &'a str,
pub debug_log_event_token: &'a DebugLogEventToken,
pub mysql_connection: sqlx::pool::PoolConnection<sqlx::MySql>,
}
/// Map the requested video model to a first-party Minimax H3 tier.
///
/// Returns `None` for every other model — including the bare `MinimaxH3`,
/// which stays on its existing third-party (Fal) route.
pub fn first_party_minimax_h3_model(
maybe_model: Option<CommonVideoModel>,
) -> Option<FirstPartyMinimaxH3Model> {
match maybe_model {
Some(CommonVideoModel::MinimaxH3Turbo) => Some(FirstPartyMinimaxH3Model::Turbo),
Some(CommonVideoModel::MinimaxH3Ultra) => Some(FirstPartyMinimaxH3Model::Ultra),
_ => None,
}
}
pub async fn enqueue_first_party_minimax_h3_job(
args: EnqueueFirstPartyMinimaxH3JobArgs<'_>,
) -> Result<Json<OmniGenVideoGenerateResponse>, CommonWebError> {
let EnqueueFirstPartyMinimaxH3JobArgs {
minimax_model,
request,
user_token,
maybe_prompt_model_type,
idempotency_token,
ip_address,
debug_log_event_token,
mut mysql_connection,
} = args;
// System credits = cents (same convention as the v2 pipeline).
let maybe_system_cost_usd_cents: Option<u64> = match minimax_model {
FirstPartyMinimaxH3Model::Turbo => {
// Turbo is free: no wallet charge.
// TODO: Gate free Turbo generations on how many jobs the user has
// already submitted. The count queries and checks land with the
// scheduler work.
None
}
FirstPartyMinimaxH3Model::Ultra => {
Some(estimate_minimax_h3_ultra_cost_usd_cents(request))
}
};
info!("First-party Minimax H3 ({:?}) estimated cost: {:?} credits",
minimax_model, maybe_system_cost_usd_cents);
// Bill the wallet (no-op deduction for the free Turbo tier) and generate
// the apriori job token.
let billing = bill_wallet(
user_token,
maybe_system_cost_usd_cents.unwrap_or(0),
&mut mysql_connection,
).await?;
let mut transaction = mysql_connection.begin().await.map_err(|err| {
error!("Error starting MySQL transaction: {:?}", err);
CommonWebError::from_error(err)
})?;
let prompt_token = write_prompt_records(WritePromptRecordsArgs {
request,
user_token,
maybe_prompt_model_type,
ip_address,
transaction: &mut transaction,
}).await;
let maybe_system_cost_u32 = maybe_system_cost_usd_cents
.and_then(|cents| u32::try_from(cents).ok());
let job_token = insert_generic_inference_job_for_first_party_minimax_h3_with_apriori_job_token(
InsertGenericInferenceForFirstPartyMinimaxH3WithAprioriJobTokenArgs {
minimax_model,
uuid_idempotency_token: idempotency_token,
apriori_job_token: &billing.apriori_job_token,
maybe_inference_args: None,
maybe_prompt_token: prompt_token.as_ref(),
maybe_wallet_ledger_entry_token: billing.maybe_wallet_ledger_entry_token.as_ref(),
maybe_system_cost_credits: maybe_system_cost_u32,
maybe_system_cost_usd_cents: maybe_system_cost_u32,
maybe_creator_user_token: Some(user_token),
maybe_avt_token: None, // AVT tokens are web-session only; API-key callers have none.
creator_ip_address: ip_address,
creator_set_visibility: Visibility::Public,
// Omni API requests are always API-key authenticated.
maybe_platform_type: Some(PlatformType::ApiKey),
maybe_debug_log_event_token: Some(debug_log_event_token),
mysql_executor: &mut *transaction,
phantom: Default::default(),
}
).await.map_err(|err| {
error!("Error inserting first-party Minimax H3 job: {:?}", err);
CommonWebError::from_error(err)
})?;
transaction.commit().await.map_err(|err| {
error!("Error committing transaction: {:?}", err);
CommonWebError::from_error(err)
})?;
Ok(Json(OmniGenVideoGenerateResponse {
success: true,
inference_job_token: job_token.clone(),
all_job_tokens: vec![job_token],
}))
}
@@ -1,101 +0,0 @@
//! Pricing for first-party Minimax H3 Ultra.
//!
//! TODO(2026-08-06): Placeholder rates copied from the existing Minimax H3
//! pricing. Finalize once first-party GPU inference costs are known.
use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_request::OmniApiVideoGenerateRequest;
use enums::common::generation::common_resolution::CommonResolution;
/// Per-second rates in hundredths of a US cent.
const LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 1_840;
const HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC: u64 = 2_990;
/// Minimax H3 defaults None → 5s.
const DEFAULT_DURATION_SECONDS: u64 = 5;
/// Estimate the user-facing Minimax H3 Ultra cost in USD cents.
/// System credits = cents (same convention as the v2 pipeline).
pub fn estimate_minimax_h3_ultra_cost_usd_cents(request: &OmniApiVideoGenerateRequest) -> u64 {
let duration_seconds = request.duration_seconds
.map(u64::from)
.unwrap_or(DEFAULT_DURATION_SECONDS);
let rate = if is_2k(request.resolution) {
HIGH_RES_RATE_HUNDREDTH_CENTS_PER_SEC
} else {
LOW_RES_RATE_HUNDREDTH_CENTS_PER_SEC
};
// Round up to the next whole cent.
(rate * duration_seconds).div_ceil(100)
}
/// Minimax H3 renders 768P or 2K; 1080p and above land on the 2K tier, while
/// 720p and below render at 768P. The default (unset) resolution is 2K.
fn is_2k(resolution: Option<CommonResolution>) -> bool {
match resolution {
None => true, // defaults to 2K
Some(CommonResolution::TenEightyP)
| Some(CommonResolution::OneK)
| Some(CommonResolution::TwoK)
| Some(CommonResolution::ThreeK)
| Some(CommonResolution::FourK) => true,
Some(CommonResolution::HalfK)
| Some(CommonResolution::FourEightyP)
| Some(CommonResolution::SevenTwentyP) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_resolution_5s_is_150() {
assert_eq!(cost_cents(None, None), 150);
}
#[test]
fn low_res_5s_is_92() {
assert_eq!(cost_cents(Some(5), Some(CommonResolution::SevenTwentyP)), 92);
}
#[test]
fn high_res_10s_is_299() {
assert_eq!(cost_cents(Some(10), Some(CommonResolution::TenEightyP)), 299);
}
#[test]
fn odd_duration_rounds_up_to_whole_cents() {
// 7s high res: 2990 × 7 = 20930 hundredth-cents → 210 cents.
assert_eq!(cost_cents(Some(7), None), 210);
}
fn cost_cents(duration_seconds: Option<u16>, resolution: Option<CommonResolution>) -> u64 {
let request = OmniApiVideoGenerateRequest {
idempotency_token: None,
model: None,
prompt: None,
negative_prompt: None,
start_frame_image_media_token: None,
start_frame_image_url: None,
end_frame_image_media_token: None,
end_frame_image_url: None,
reference_image_media_tokens: None,
reference_image_urls: None,
reference_video_media_tokens: None,
reference_video_urls: None,
reference_audio_media_tokens: None,
reference_audio_urls: None,
reference_character_tokens: None,
resolution,
aspect_ratio: None,
bitrate: None,
quality: None,
duration_seconds,
video_batch_count: None,
generate_audio: None,
};
estimate_minimax_h3_ultra_cost_usd_cents(&request)
}
}
@@ -1,8 +0,0 @@
//! First-party Minimax H3 (Turbo / Ultra) video generation.
//!
//! These models are fulfilled by our own GPU inference rather than an
//! external provider, so enqueueing only writes database records — a
//! scheduler runs the jobs later.
pub mod enqueue_first_party_minimax_h3_job;
pub mod minimax_h3_ultra_cost;
@@ -4,4 +4,3 @@ pub(crate) mod hydrate_router_request;
pub mod pipeline_result;
pub mod resolve_kinovi_character_ids;
pub mod resolve_media_tokens_to_urls;
pub mod write_prompt_records;
@@ -1,132 +0,0 @@
//! Shared prompt bookkeeping for the omni-api video generate flows.
//!
//! Writes the prompt row and its context items (start/end frames, image and
//! video references) inside the caller's transaction. Best-effort: failures
//! are logged and yield `None` — job enqueue must not fail because prompt
//! bookkeeping did.
use log::warn;
use sqlx::{MySql, Transaction};
use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_request::OmniApiVideoGenerateRequest;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation_provider::GenerationProvider;
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{
insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem,
};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use tokens::tokens::prompts::PromptToken;
use tokens::tokens::users::UserToken;
pub struct WritePromptRecordsArgs<'a, 'tx> {
pub request: &'a OmniApiVideoGenerateRequest,
pub user_token: &'a UserToken,
pub maybe_prompt_model_type: Option<CommonModelType>,
pub ip_address: &'a str,
pub transaction: &'a mut Transaction<'tx, MySql>,
}
pub async fn write_prompt_records(args: WritePromptRecordsArgs<'_, '_>) -> Option<PromptToken> {
let WritePromptRecordsArgs {
request,
user_token,
maybe_prompt_model_type,
ip_address,
transaction,
} = args;
let prompt_token = match insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: Some(user_token),
maybe_model_type: maybe_prompt_model_type,
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: request.negative_prompt.as_deref(),
maybe_other_args: None,
maybe_generation_mode: Some(determine_generation_mode(request)),
maybe_aspect_ratio: request.aspect_ratio,
maybe_resolution: request.resolution,
maybe_bitrate: request.bitrate,
maybe_batch_count: request.video_batch_count.map(|c| c as u8),
maybe_generate_audio: request.generate_audio,
maybe_duration_seconds: request.duration_seconds.map(|d| d as u32),
creator_ip_address: ip_address,
mysql_executor: &mut **transaction,
phantom: Default::default(),
}).await {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None
}
};
if let Some(token) = prompt_token.as_ref() {
let mut context_items = Vec::new();
if let Some(media_token) = &request.start_frame_image_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidStartFrame,
});
}
if let Some(media_token) = &request.end_frame_image_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidEndFrame,
});
}
if let Some(ref_tokens) = &request.reference_image_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
});
}
}
if let Some(ref_tokens) = &request.reference_video_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidRef,
});
}
}
if !context_items.is_empty() {
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: context_items,
transaction,
}).await {
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
prompt_token
}
fn determine_generation_mode(request: &OmniApiVideoGenerateRequest) -> CommonGenerationMode {
let has_keyframe = request.start_frame_image_media_token.is_some()
|| request.end_frame_image_media_token.is_some();
if has_keyframe {
return CommonGenerationMode::Keyframe;
}
let has_reference = request.reference_image_media_tokens.as_ref().is_some_and(|t| !t.is_empty())
|| request.reference_video_media_tokens.as_ref().is_some_and(|t| !t.is_empty())
|| request.reference_audio_media_tokens.as_ref().is_some_and(|t| !t.is_empty())
|| request.reference_character_tokens.as_ref().is_some_and(|t| !t.is_empty());
if has_reference {
return CommonGenerationMode::Reference;
}
CommonGenerationMode::Text
}
@@ -1,6 +1,5 @@
pub mod omni_api_video_generate_handler;
pub mod check_request;
pub mod first_party_minimax_h3;
pub mod helpers;
pub mod ingest_url_inputs;
pub mod pipeline_v2;
@@ -10,14 +10,22 @@ use artcraft_api_defs::omni_api::generate_requests::omni_api_video_generate_requ
use artcraft_api_defs::omni_gen::generate_response::omni_gen_video_generate_response::OmniGenVideoGenerateResponse;
use artcraft_router::generate::generate_video::generate_video_response::GenerateVideoResponse;
use enums::by_table::debug_logs::debug_log_type::DebugLogType;
use enums::by_table::prompt_context_items::prompt_context_semantic_type::PromptContextSemanticType;
use enums::by_table::prompts::prompt_type::PromptType;
use enums::common::generation::common_generation_mode::CommonGenerationMode;
use enums::common::generation::common_model_type::CommonModelType;
use enums::common::generation::common_video_model::CommonVideoModel;
use enums::common::generation_provider::GenerationProvider;
use enums::common::platform_type::PlatformType;
use http_server_common::request::get_request_ip::get_request_ip;
use enums::by_table::debug_logs::debug_log_level::DebugLogLevel;
use mysql_queries::queries::debug_logs::insert_debug_log::{insert_debug_log, InsertDebugLogArgs};
use mysql_queries::queries::generic_inference::api_providers::kinovi_web::insert_generic_inference_job_for_kinovi_web_queue_with_apriori_job_token::KinoviVersion;
use mysql_queries::queries::idepotency_tokens::insert_idempotency_token::insert_idempotency_token;
use mysql_queries::queries::prompt_context_items::insert_batch_prompt_context_items::{
insert_batch_prompt_context_items, InsertBatchArgs, PromptContextItem,
};
use mysql_queries::queries::prompts::insert_prompt::{insert_prompt, InsertPromptArgs};
use tokens::tokens::characters::CharacterToken;
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
use tokens::tokens::media_files::MediaFileToken;
@@ -27,9 +35,6 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::generate::common::generation_debug_logs::GenerationDebugLogContext;
use crate::http_server::endpoints::generate::common::payments_error_test::payments_error_test;
use crate::http_server::endpoints::omni_api::generate::video::check_request::check_request;
use crate::http_server::endpoints::omni_api::generate::video::first_party_minimax_h3::enqueue_first_party_minimax_h3_job::{
enqueue_first_party_minimax_h3_job, first_party_minimax_h3_model, EnqueueFirstPartyMinimaxH3JobArgs,
};
use crate::http_server::endpoints::omni_api::generate::video::ingest_url_inputs::ingest_url_inputs;
use crate::http_server::endpoints::omni_api::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_api::generate::video::helpers::resolve_kinovi_character_ids::resolve_kinovi_character_ids;
@@ -38,9 +43,6 @@ use crate::http_server::endpoints::omni_api::generate::video::insert_db_job::ins
use crate::http_server::endpoints::omni_api::generate::video::insert_db_job::insert_grok_api_job::{insert_grok_api_job, InsertGrokApiJobArgs};
use crate::http_server::endpoints::omni_api::generate::video::insert_db_job::insert_kinovi_web_jobs::{insert_kinovi_web_jobs, InsertKinoviWebJobsArgs};
use crate::http_server::endpoints::omni_api::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
use crate::http_server::endpoints::omni_api::generate::video::helpers::write_prompt_records::{
write_prompt_records, WritePromptRecordsArgs,
};
use crate::http_server::endpoints::omni_api::shared_utils::kinovi_account::KinoviAccount;
use crate::http_server::endpoints::omni_api::generate::video::pipeline_v2::run_pipeline_v2::{run_pipeline_v2, RunPipelineV2Args};
use crate::http_server::endpoints::omni_api::shared_utils::video::validate_video_request::validate_video_request;
@@ -216,24 +218,6 @@ pub async fn omni_api_video_generate_handler(
warn!("Failed to insert HTTP request debug log: {:?}", err);
}
// ==================== FIRST-PARTY MINIMAX H3 (TURBO / ULTRA) ==================== //
// These models run on our own GPU inference — no provider call is made.
// The module bills the wallet (Ultra only), writes the prompt + job
// records, and returns; a scheduler picks the pending jobs up later.
if let Some(minimax_model) = first_party_minimax_h3_model(request.model) {
return enqueue_first_party_minimax_h3_job(EnqueueFirstPartyMinimaxH3JobArgs {
minimax_model,
request: &request,
user_token,
maybe_prompt_model_type,
idempotency_token: &idempotency_token,
ip_address: &ip_address,
debug_log_event_token: &debug_log_event_token,
mysql_connection,
}).await;
}
// NB: The pipeline takes over the connection for its remaining pre-request DB writes (billing,
// outbound provider request debug log) and releases it before the (slow, external) generation
// call — holding a pool slot across that call is what starves the pool and causes PoolTimedOut
@@ -297,15 +281,79 @@ pub async fn omni_api_video_generate_handler(
CommonWebError::from_error(err)
})?;
// -- Prompt + context items --
// -- Prompt --
let prompt_token = write_prompt_records(WritePromptRecordsArgs {
request: &request,
user_token,
maybe_prompt_model_type,
ip_address: &ip_address,
transaction: &mut transaction,
}).await;
let prompt_token = match insert_prompt(InsertPromptArgs {
maybe_apriori_prompt_token: None,
prompt_type: PromptType::ArtcraftApp,
maybe_creator_user_token: Some(user_token),
maybe_model_type: maybe_prompt_model_type,
maybe_generation_provider: Some(GenerationProvider::Artcraft),
maybe_positive_prompt: request.prompt.as_deref(),
maybe_negative_prompt: request.negative_prompt.as_deref(),
maybe_other_args: None,
maybe_generation_mode: Some(determine_generation_mode(&request)),
maybe_aspect_ratio: request.aspect_ratio,
maybe_resolution: request.resolution,
maybe_bitrate: request.bitrate,
maybe_batch_count: request.video_batch_count.map(|c| c as u8),
maybe_generate_audio: request.generate_audio,
maybe_duration_seconds: request.duration_seconds.map(|d| d as u32),
creator_ip_address: &ip_address,
mysql_executor: &mut *transaction,
phantom: Default::default(),
}).await {
Ok(token) => Some(token),
Err(err) => {
warn!("Error inserting prompt: {:?}", err);
None
}
};
// -- Prompt context items --
if let Some(token) = prompt_token.as_ref() {
let mut context_items = Vec::new();
if let Some(media_token) = &request.start_frame_image_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidStartFrame,
});
}
if let Some(media_token) = &request.end_frame_image_media_token {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidEndFrame,
});
}
if let Some(ref_tokens) = &request.reference_image_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::Imgref,
});
}
}
if let Some(ref_tokens) = &request.reference_video_media_tokens {
for media_token in ref_tokens {
context_items.push(PromptContextItem {
media_token: media_token.clone(),
context_semantic_type: PromptContextSemanticType::VidRef,
});
}
}
if !context_items.is_empty() {
if let Err(err) = insert_batch_prompt_context_items(InsertBatchArgs {
prompt_token: token.clone(),
items: context_items,
transaction: &mut transaction,
}).await {
warn!("Error inserting batch prompt context items: {:?}", err);
}
}
}
// -- Inference job --
@@ -439,3 +487,23 @@ pub async fn omni_api_video_generate_handler(
all_job_tokens,
}))
}
fn determine_generation_mode(request: &OmniApiVideoGenerateRequest) -> CommonGenerationMode {
let has_keyframe = request.start_frame_image_media_token.is_some()
|| request.end_frame_image_media_token.is_some();
if has_keyframe {
return CommonGenerationMode::Keyframe;
}
let has_reference = request.reference_image_media_tokens.as_ref().is_some_and(|t| !t.is_empty())
|| request.reference_video_media_tokens.as_ref().is_some_and(|t| !t.is_empty())
|| request.reference_audio_media_tokens.as_ref().is_some_and(|t| !t.is_empty())
|| request.reference_character_tokens.as_ref().is_some_and(|t| !t.is_empty());
if has_reference {
return CommonGenerationMode::Reference;
}
CommonGenerationMode::Text
}