mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-23 18:19:30 -04:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
45d08d6f79 | ||
|
|
a10f36b652 | ||
|
|
e64dc5245e | ||
|
|
6e3a848da7 | ||
|
|
03e816edc2 | ||
|
|
0147ac6fa3 | ||
|
|
59cc03db09 | ||
|
|
718d4ec4ee | ||
|
|
1f943bff01 | ||
|
|
0379bb3187 | ||
|
|
8672b5a05b | ||
|
|
0701d718ee | ||
|
|
086c43b059 | ||
|
|
0640493867 | ||
|
|
1a10c21f34 |
Generated
+36
@@ -803,6 +803,7 @@ dependencies = [
|
||||
"enums",
|
||||
"fal_client",
|
||||
"gmicloud_client",
|
||||
"grok_api_client",
|
||||
"log",
|
||||
"muapi_client",
|
||||
"reqwest 0.12.23",
|
||||
@@ -4897,6 +4898,40 @@ dependencies = [
|
||||
"web-time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grok-api-job"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"actix_helpers",
|
||||
"anyhow",
|
||||
"bootstrap",
|
||||
"bucket_paths",
|
||||
"cloud_storage",
|
||||
"concurrency",
|
||||
"easyenv",
|
||||
"enums",
|
||||
"errors",
|
||||
"grok_api_client",
|
||||
"hashing",
|
||||
"jobs_common",
|
||||
"log",
|
||||
"mysql_queries",
|
||||
"pager",
|
||||
"reqwest 0.12.23",
|
||||
"rootly_client",
|
||||
"rootly_config",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"server_environment",
|
||||
"shared_env_var_config",
|
||||
"sqlx",
|
||||
"tokens",
|
||||
"tokio",
|
||||
"workspace-build-acceleration",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "grok_api_client"
|
||||
version = "0.0.1"
|
||||
@@ -11518,6 +11553,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"gmicloud_client",
|
||||
"google_sign_in",
|
||||
"grok_api_client",
|
||||
"hashing",
|
||||
"hostname 0.4.0",
|
||||
"http_server_common",
|
||||
|
||||
@@ -128,6 +128,7 @@ members = [
|
||||
|
||||
# Services: API servers, jobs, workers, etc.
|
||||
"crates/service/job/gmicloud_job",
|
||||
"crates/service/job/grok_api_job",
|
||||
"crates/service/job/seedance2_pro_job",
|
||||
"crates/service/job/video_thumbnail_job",
|
||||
"crates/service/job/worldlabs_job",
|
||||
|
||||
+7
-1
@@ -149,7 +149,12 @@ pub struct OmniGenVideoModelDetails {
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_max: Option<u16>,
|
||||
|
||||
|
||||
/// Specific maximum when image references are used,
|
||||
/// eg. Grok limits to 10 seconds in this mode.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_max_with_image_references: Option<u16>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub duration_seconds_options: Option<Vec<u16>>,
|
||||
|
||||
@@ -204,6 +209,7 @@ impl Default for OmniGenVideoModelDetails {
|
||||
default_quality: None,
|
||||
duration_seconds_min: None,
|
||||
duration_seconds_max: None,
|
||||
duration_seconds_max_with_image_references: None,
|
||||
duration_seconds_options: None,
|
||||
duration_seconds_default: None,
|
||||
batch_size_min: None,
|
||||
|
||||
@@ -21,6 +21,7 @@ enums.workspace = true
|
||||
artcraft_client.workspace = true
|
||||
fal_client.workspace = true
|
||||
gmicloud_client.workspace = true
|
||||
grok_api_client.workspace = true
|
||||
muapi_client.workspace = true
|
||||
seedance2pro_client.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
@@ -19,6 +19,10 @@ pub enum CommonImageModel {
|
||||
GptImage1p5,
|
||||
#[serde(rename = "gpt_image_2")]
|
||||
GptImage2,
|
||||
#[serde(rename = "grok_imagine_image")]
|
||||
GrokImagineImage,
|
||||
#[serde(rename = "grok_imagine_image_q")]
|
||||
GrokImagineImageQuality,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
|
||||
@@ -8,6 +8,9 @@ pub enum CommonVideoModel {
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ pub enum Provider {
|
||||
Artcraft,
|
||||
Fal,
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
Muapi,
|
||||
Seedance2Pro,
|
||||
}
|
||||
|
||||
@@ -7,5 +7,6 @@ pub mod router_client;
|
||||
pub mod router_fal_client;
|
||||
pub mod router_fal_webhook_optional_client;
|
||||
pub mod router_gmicloud_client;
|
||||
pub mod router_grok_api_client;
|
||||
pub mod router_muapi_client;
|
||||
pub mod router_seedance2pro_client;
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
@@ -11,6 +12,7 @@ pub struct MultiRouterClient {
|
||||
pub(crate) fal_client: Option<RouterFalClient>,
|
||||
pub(crate) fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
|
||||
pub(crate) gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
pub(crate) grok_api_client: Option<RouterGrokApiClient>,
|
||||
pub(crate) muapi_client: Option<RouterMuapiClient>,
|
||||
pub(crate) seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
}
|
||||
@@ -36,6 +38,11 @@ impl MultiRouterClient {
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::GmiCloud))
|
||||
}
|
||||
|
||||
pub fn get_grok_api_client_ref(&self) -> Result<&RouterGrokApiClient, ClientError> {
|
||||
self.grok_api_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::GrokApi))
|
||||
}
|
||||
|
||||
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
|
||||
self.muapi_client.as_ref()
|
||||
.ok_or(ClientError::ClientNotConfigured(ClientType::Muapi))
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
|
||||
@@ -11,6 +12,7 @@ pub struct MultiRouterClientBuilder {
|
||||
fal_client: Option<RouterFalClient>,
|
||||
fal_webhook_optional_client: Option<RouterFalWebhookOptionalClient>,
|
||||
gmicloud_client: Option<RouterGmiCloudClient>,
|
||||
grok_api_client: Option<RouterGrokApiClient>,
|
||||
muapi_client: Option<RouterMuapiClient>,
|
||||
seedance2pro_client: Option<RouterSeedance2ProClient>,
|
||||
}
|
||||
@@ -22,6 +24,7 @@ impl MultiRouterClientBuilder {
|
||||
fal_client: None,
|
||||
fal_webhook_optional_client: None,
|
||||
gmicloud_client: None,
|
||||
grok_api_client: None,
|
||||
muapi_client: None,
|
||||
seedance2pro_client: None,
|
||||
}
|
||||
@@ -47,6 +50,11 @@ impl MultiRouterClientBuilder {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_grok_api_client(mut self, client: RouterGrokApiClient) -> Self {
|
||||
self.grok_api_client = Some(client);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_muapi_client(mut self, client: RouterMuapiClient) -> Self {
|
||||
self.muapi_client = Some(client);
|
||||
self
|
||||
@@ -63,6 +71,7 @@ impl MultiRouterClientBuilder {
|
||||
fal_client: self.fal_client,
|
||||
fal_webhook_optional_client: self.fal_webhook_optional_client,
|
||||
gmicloud_client: self.gmicloud_client,
|
||||
grok_api_client: self.grok_api_client,
|
||||
muapi_client: self.muapi_client,
|
||||
seedance2pro_client: self.seedance2pro_client,
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::client::router_fal_client::RouterFalClient;
|
||||
use crate::client::router_fal_webhook_optional_client::RouterFalWebhookOptionalClient;
|
||||
use crate::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::client::router_muapi_client::RouterMuapiClient;
|
||||
use crate::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use crate::errors::client_error::{ClientError, ClientType};
|
||||
@@ -13,6 +14,7 @@ pub enum RouterClient {
|
||||
Fal(RouterFalClient),
|
||||
FalWebhookOptional(RouterFalWebhookOptionalClient),
|
||||
GmiCloud(RouterGmiCloudClient),
|
||||
GrokApi(RouterGrokApiClient),
|
||||
Muapi(RouterMuapiClient),
|
||||
Seedance2Pro(RouterSeedance2ProClient),
|
||||
}
|
||||
@@ -50,6 +52,14 @@ impl RouterClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_grok_api_client_ref(&self) -> Result<&RouterGrokApiClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::GrokApi(client) => Ok(client),
|
||||
RouterClient::Multi(multi) => multi.get_grok_api_client_ref(),
|
||||
_ => Err(ClientError::ClientNotConfigured(ClientType::GrokApi)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_muapi_client_ref(&self) -> Result<&RouterMuapiClient, ClientError> {
|
||||
match self {
|
||||
RouterClient::Muapi(client) => Ok(client),
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use grok_api_client::creds::grok_api_key::GrokApiKey;
|
||||
|
||||
pub struct RouterGrokApiClient {
|
||||
pub(crate) api_key: GrokApiKey,
|
||||
}
|
||||
|
||||
impl RouterGrokApiClient {
|
||||
pub fn new(api_key: GrokApiKey) -> Self {
|
||||
RouterGrokApiClient { api_key }
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ pub enum ClientType {
|
||||
Fal,
|
||||
FalWebhookOptional,
|
||||
GmiCloud,
|
||||
GrokApi,
|
||||
Muapi,
|
||||
Seedance2Pro,
|
||||
}
|
||||
@@ -20,6 +21,7 @@ impl Display for ClientType {
|
||||
Self::Fal => write!(f, "Fal"),
|
||||
Self::FalWebhookOptional => write!(f, "FalWebhookOptional"),
|
||||
Self::GmiCloud => write!(f, "GmiCloud"),
|
||||
Self::GrokApi => write!(f, "GrokApi"),
|
||||
Self::Muapi => write!(f, "Muapi"),
|
||||
Self::Seedance2Pro => write!(f, "Seedance2Pro"),
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ use std::fmt::{Display, Formatter};
|
||||
use artcraft_client::error::storyteller_error::StorytellerError;
|
||||
use fal_client::error::fal_error_plus::FalErrorPlus;
|
||||
use gmicloud_client::error::gmicloud_error::GmiCloudError;
|
||||
use grok_api_client::error::grok_error::GrokError;
|
||||
use muapi_client::error::muapi_error::MuapiError;
|
||||
use seedance2pro_client::error::seedance2pro_error::Seedance2ProError;
|
||||
|
||||
@@ -11,6 +12,7 @@ pub enum ProviderError {
|
||||
Storyteller(StorytellerError),
|
||||
Fal(FalErrorPlus),
|
||||
GmiCloud(GmiCloudError),
|
||||
Grok(GrokError),
|
||||
Muapi(MuapiError),
|
||||
Seedance2Pro(Seedance2ProError),
|
||||
}
|
||||
@@ -23,6 +25,7 @@ impl Display for ProviderError {
|
||||
Self::Storyteller(e) => write!(f, "Storyteller provider error: {}", e),
|
||||
Self::Fal(e) => write!(f, "Fal provider error: {}", e),
|
||||
Self::GmiCloud(e) => write!(f, "GmiCloud provider error: {}", e),
|
||||
Self::Grok(e) => write!(f, "Grok provider error: {}", e),
|
||||
Self::Muapi(e) => write!(f, "Muapi provider error: {}", e),
|
||||
Self::Seedance2Pro(e) => write!(f, "Seedance2Pro provider error: {}", e),
|
||||
}
|
||||
@@ -47,6 +50,12 @@ impl From<GmiCloudError> for ProviderError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<GrokError> for ProviderError {
|
||||
fn from(error: GrokError) -> Self {
|
||||
Self::Grok(error)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<MuapiError> for ProviderError {
|
||||
fn from(error: MuapiError) -> Self {
|
||||
Self::Muapi(error)
|
||||
|
||||
+9
@@ -47,6 +47,7 @@ use crate::generate::generate_video::plan::muapi::plan_generate_video_muapi_seed
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0::plan_generate_video_seedance2pro_seedance2p0;
|
||||
use crate::generate::generate_video::plan::seedance2pro::plan_generate_video_seedance2pro_seedance2p0_fast::plan_generate_video_seedance2pro_seedance2p0_fast;
|
||||
use crate::generate::generate_video::video_generation_plan::VideoGenerationPlan;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::build::build_artcraft_grok_imagine_video;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::build::build_artcraft_happy_horse_1p0;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::build::build_artcraft_preview_model;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model_fast::build::build_artcraft_preview_model_fast;
|
||||
@@ -60,6 +61,7 @@ use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::buil
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::build::build_kinovi_seedance_2p0;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::build::build_gmicloud_seedance_2p0_u;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::build::build_gmicloud_seedance_2p0_u_fast;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::build::build_grok_api_grok_imagine_video;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::build::build_kinovi_seedance_2p0_fast;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
@@ -153,6 +155,7 @@ impl GenerateVideoRequestBuilder {
|
||||
pub fn use_new_builder(&self) -> bool {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(Provider::Artcraft, CommonVideoModel::GrokImagineVideo) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0) => true,
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => true,
|
||||
@@ -165,6 +168,8 @@ impl GenerateVideoRequestBuilder {
|
||||
// GmiCloud
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => true,
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => true,
|
||||
// Grok
|
||||
(Provider::GrokApi, CommonVideoModel::GrokImagineVideo) => true,
|
||||
// Kinovi
|
||||
(Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => true,
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => true,
|
||||
@@ -177,6 +182,7 @@ impl GenerateVideoRequestBuilder {
|
||||
pub fn build2(self) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
match (self.provider, self.model) {
|
||||
// Artcraft
|
||||
(Provider::Artcraft, CommonVideoModel::GrokImagineVideo) => build_artcraft_grok_imagine_video(self),
|
||||
(Provider::Artcraft, CommonVideoModel::HappyHorse1p0) => build_artcraft_happy_horse_1p0(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0) => build_artcraft_seedance_2p0(self),
|
||||
(Provider::Artcraft, CommonVideoModel::Seedance2p0Fast) => build_artcraft_seedance_2p0_fast(self),
|
||||
@@ -189,6 +195,8 @@ impl GenerateVideoRequestBuilder {
|
||||
// GmiCloud
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0Ultra) => build_gmicloud_seedance_2p0_u(self),
|
||||
(Provider::GmiCloud, CommonVideoModel::Seedance2p0UltraFast) => build_gmicloud_seedance_2p0_u_fast(self),
|
||||
// Grok
|
||||
(Provider::GrokApi, CommonVideoModel::GrokImagineVideo) => build_grok_api_grok_imagine_video(self),
|
||||
// Kinovi
|
||||
(Provider::Seedance2Pro, CommonVideoModel::HappyHorse1p0) => build_kinovi_happy_horse_1p0(self),
|
||||
(Provider::Seedance2Pro, CommonVideoModel::Seedance2p0) => build_kinovi_seedance_2p0(self),
|
||||
@@ -203,6 +211,7 @@ impl GenerateVideoRequestBuilder {
|
||||
Provider::Artcraft => self.build_artcraft(),
|
||||
Provider::Fal => self.build_fal(),
|
||||
Provider::GmiCloud => self.unsupported_provider(), // GmiCloud uses build2() only
|
||||
Provider::GrokApi => self.unsupported_provider(), // Grok uses build2() only
|
||||
Provider::Muapi => self.build_muapi(),
|
||||
Provider::Seedance2Pro => self.build_seedance2pro(),
|
||||
}
|
||||
|
||||
+20
@@ -39,10 +39,23 @@ pub struct GmiCloudVideoResponsePayload {
|
||||
pub request_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GrokVideoResponsePayload {
|
||||
/// xAI request_id; poll with `grok_api_client::api::requests::videos::video_status`
|
||||
/// to fetch the rendered video URL once it's ready.
|
||||
pub request_id: String,
|
||||
|
||||
/// The outbound request that was sent to Grok (xAI).
|
||||
/// Stored as a trait object so any Request type can be captured.
|
||||
/// Use `format!("{:?}", ...)` or `format!("{:#?}", ...)` to print.
|
||||
pub maybe_outbound_request: Option<Arc<dyn Debug + Send + Sync>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GenerateVideoResponse {
|
||||
Artcraft(ArtcraftVideoResponsePayload),
|
||||
GmiCloud(GmiCloudVideoResponsePayload),
|
||||
Grok(GrokVideoResponsePayload),
|
||||
Muapi(MuapiVideoResponsePayload),
|
||||
Seedance2Pro(Seedance2proVideoResponsePayload),
|
||||
Fal(FalVideoResponsePayload),
|
||||
@@ -77,6 +90,13 @@ impl GenerateVideoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_grok_payload(&self) -> Option<GrokVideoResponsePayload> {
|
||||
match self {
|
||||
Self::Grok(p) => Some(p.clone()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_fal_payload(&self) -> Option<FalVideoResponsePayload> {
|
||||
match self {
|
||||
Self::Fal(p) => Some(p.clone()),
|
||||
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::build_common::{
|
||||
build_artcraft_omni_request, SupportedResolutions, UltraWideSupport,
|
||||
};
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::request::ArtcraftGrokImagineVideoRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
/// Build an ArtCraft-routed Grok Imagine Video request.
|
||||
///
|
||||
/// xAI's grok-imagine-video supports 480p and 720p (no 1080p — see
|
||||
/// VideoResolution docs in grok_api_client), and does NOT have a 21:9
|
||||
/// ultra-wide aspect ratio. Hence `Fast` resolution support and
|
||||
/// `Unsupported` ultra-wide.
|
||||
pub fn build_artcraft_grok_imagine_video(
|
||||
builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let request = build_artcraft_omni_request(
|
||||
builder,
|
||||
CommonVideoModelEnum::GrokImagineVideo,
|
||||
SupportedResolutions::Fast,
|
||||
UltraWideSupport::Unsupported,
|
||||
)?;
|
||||
let state = ArtcraftGrokImagineVideoRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo(state),
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use enums::common::generation::common_resolution::CommonResolution as CommonResolutionEnum;
|
||||
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
|
||||
use tokens::tokens::characters::CharacterToken;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::character_list_ref::CharacterListRef;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
use super::*;
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn model_is_grok_imagine_video() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(matches!(req.request.model, Some(CommonVideoModelEnum::GrokImagineVideo)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test".to_string()); }));
|
||||
assert_eq!(req.request.prompt, Some("test".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(10); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(10));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration_seconds, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_count_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.video_batch_count = Some(4); }));
|
||||
assert_eq!(req.request.video_batch_count, Some(4));
|
||||
}
|
||||
}
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_downgrades_to_720p() {
|
||||
// SupportedResolutions::Fast caps at 720p (Grok Imagine doesn't render 1080p output).
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(CommonResolutionEnum::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
mod media_token_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_token_passed_through() {
|
||||
let token = MediaFileToken::new("mf_start".to_string());
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(token.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.start_frame_image_media_token, Some(token));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn url_start_frame_rejected() {
|
||||
// ArtCraft's omni endpoint requires media tokens — URLs are rejected
|
||||
// (unlike the direct Grok provider, which is URL-only).
|
||||
let result = build_artcraft_grok_imagine_video(GenerateVideoRequestBuilder {
|
||||
start_frame: Some(ImageRef::Url("https://example.com".to_string())),
|
||||
..base_builder()
|
||||
});
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_passed_through() {
|
||||
let tokens = vec![MediaFileToken::new("mf_a".to_string()), MediaFileToken::new("mf_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_image_media_tokens, Some(tokens));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_tokens_passed_through() {
|
||||
let tokens = vec![CharacterToken::new("char_a".to_string()), CharacterToken::new("char_b".to_string())];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_character_tokens = Some(CharacterListRef::CharacterTokens(tokens.clone()));
|
||||
}));
|
||||
assert_eq!(req.request.reference_character_tokens, Some(tokens));
|
||||
}
|
||||
}
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
provider: Provider::Artcraft,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut builder = base_builder();
|
||||
f(&mut builder);
|
||||
builder
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> ArtcraftGrokImagineVideoRequestState {
|
||||
let result = build_artcraft_grok_imagine_video(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo(state)
|
||||
) => state,
|
||||
_ => panic!("expected ArtcraftGrokImagineVideo request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
use enums::common::generation::common_resolution::CommonResolution;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::request::ArtcraftGrokImagineVideoRequestState;
|
||||
|
||||
// -- Pricing constants --
|
||||
//
|
||||
// ArtCraft credits: 100 credits = $1.00. Credits always equal USD cents.
|
||||
//
|
||||
// We keep these as f64 because per-second rates are fractional; rounding
|
||||
// happens once at the end after multiplying by duration * batch.
|
||||
|
||||
const CENTS_PER_SECOND_480P: f64 = 6.5;
|
||||
const CENTS_PER_SECOND_720P: f64 = 9.1;
|
||||
|
||||
// Per-source-image surcharge. Kept as a separate term so it doesn't get
|
||||
// lost in rounding when the per-second video cost is small.
|
||||
const CENTS_PER_INPUT_IMAGE: f64 = 0.26;
|
||||
|
||||
pub struct ArtcraftGrokImagineVideoCostState {
|
||||
pub resolution: CommonResolution,
|
||||
pub duration_seconds: u16,
|
||||
pub batch_count: u16,
|
||||
pub input_image_count: u64,
|
||||
}
|
||||
|
||||
impl ArtcraftGrokImagineVideoCostState {
|
||||
pub fn from_request(request: &ArtcraftGrokImagineVideoRequestState) -> Self {
|
||||
// Default duration is 8s when omitted, matching the upstream model's
|
||||
// default — keeps cost estimates from reading 0 for under-specified
|
||||
// requests.
|
||||
let resolution = request.request.resolution.unwrap_or(CommonResolution::SevenTwentyP);
|
||||
let duration_seconds = request.request.duration_seconds.unwrap_or(8);
|
||||
let batch_count = request.request.video_batch_count.unwrap_or(1);
|
||||
|
||||
// Mirror grok_api_client's input-image counting: start_frame + reference_images.
|
||||
let input_image_count = (request.request.start_frame_image_media_token.is_some() as u64)
|
||||
+ (request.request.reference_image_media_tokens
|
||||
.as_ref()
|
||||
.map(|v| v.len() as u64)
|
||||
.unwrap_or(0));
|
||||
|
||||
Self { resolution, duration_seconds, batch_count, input_image_count }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
let cents_per_second = match self.resolution {
|
||||
CommonResolution::FourEightyP => CENTS_PER_SECOND_480P,
|
||||
// Grok Imagine Video caps output at 720p; price anything else as 720p.
|
||||
_ => CENTS_PER_SECOND_720P,
|
||||
};
|
||||
|
||||
let video_cents = self.duration_seconds as f64 * cents_per_second * self.batch_count as f64;
|
||||
// Input images are billed once, not per output in the batch.
|
||||
let input_cents = self.input_image_count as f64 * CENTS_PER_INPUT_IMAGE;
|
||||
|
||||
let usd_cents = (video_cents + input_cents).ceil() as u64;
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: Some(usd_cents),
|
||||
cost_in_usd_cents: Some(usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
|
||||
// ── 720p pricing (9.1 ¢/s base × batch, ceil) ──
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
// 9.1 × 5 = 45.5 → 46
|
||||
// 9.1 × 10 = 91.0 → 91
|
||||
// 9.1 × 15 = 136.5 → 137
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1), 46);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1), 91);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1), 137);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
// 9.1 × 5 × 2 = 91 → 91
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2), 91);
|
||||
// 9.1 × 15 × 2 = 273 → 273
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, 2), 273);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
// 9.1 × 5 × 4 = 182 → 182
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4), 182);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_defaults_to_720p() {
|
||||
assert_eq!(
|
||||
cost_cents(None, 5, 1),
|
||||
cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 480p pricing (6.5 ¢/s base × batch, ceil) ──
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn batch_1() {
|
||||
// 6.5 × 5 = 32.5 → 33
|
||||
// 6.5 × 10 = 65.0 → 65
|
||||
// 6.5 × 15 = 97.5 → 98
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 1), 33);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, 1), 65);
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, 1), 98);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_2() {
|
||||
// 6.5 × 5 × 2 = 65 → 65
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 2), 65);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_4() {
|
||||
// 6.5 × 5 × 4 = 130 → 130
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, 4), 130);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Input-image surcharge ──
|
||||
|
||||
mod input_image_surcharge {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_start_frame_adds_input_charge() {
|
||||
// 720p 5s batch 1 = 45.5 + 0.26 (one start_frame) = 45.76 → ceil 46
|
||||
// Identical to the no-image case (45.5 → 46) because of ceil rounding.
|
||||
let with_img = cost_cents_with_images(Some(CommonResolution::SevenTwentyP), 5, 1, true, 0);
|
||||
let no_img = cost_cents_with_images(Some(CommonResolution::SevenTwentyP), 5, 1, false, 0);
|
||||
assert_eq!(with_img, 46);
|
||||
assert_eq!(no_img, 46);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn many_reference_images_eventually_bump_a_cent() {
|
||||
// 480p 4s batch 1 = 6.5 × 4 = 26 + 0.26 × N
|
||||
// N=0 → 26
|
||||
// N=1 → 26.26 → 27
|
||||
// N=4 → 27.04 → 28
|
||||
assert_eq!(cost_cents_with_images(Some(CommonResolution::FourEightyP), 4, 1, false, 0), 26);
|
||||
assert_eq!(cost_cents_with_images(Some(CommonResolution::FourEightyP), 4, 1, false, 1), 27);
|
||||
assert_eq!(cost_cents_with_images(Some(CommonResolution::FourEightyP), 4, 1, false, 4), 28);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Relative pricing ──
|
||||
|
||||
mod relative_pricing {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cost_480p_cheaper_than_720p() {
|
||||
let c480 = cost_cents(Some(CommonResolution::FourEightyP), 10, 1);
|
||||
let c720 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1);
|
||||
assert!(c480 < c720, "480p ({c480}) should be cheaper than 720p ({c720})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_duration() {
|
||||
let c5 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
let c10 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, 1);
|
||||
let c15 = cost_cents(Some(CommonResolution::SevenTwentyP), 15, 1);
|
||||
assert!(c5 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cost_scales_with_batch() {
|
||||
let b1 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 1);
|
||||
let b2 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 2);
|
||||
let b4 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, 4);
|
||||
assert!(b1 < b2);
|
||||
assert!(b2 < b4);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Credits equal cents ──
|
||||
|
||||
mod credits_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn credits_equal_usd_cents_all_combos() {
|
||||
let resolutions = [
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
None,
|
||||
];
|
||||
for res in resolutions {
|
||||
for dur in [4, 5, 10, 15] {
|
||||
for batch in [1, 2, 4] {
|
||||
let cost = build_cost(res, dur, batch);
|
||||
assert_eq!(
|
||||
cost.cost_in_credits, cost.cost_in_usd_cents,
|
||||
"credits should equal cents for res={:?} dur={}s batch={}",
|
||||
res, dur, batch,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-provider margin: ArtCraft is consistently ~30% above GrokApi ──
|
||||
|
||||
mod small_margin {
|
||||
use super::*;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
|
||||
#[test]
|
||||
fn small_margin() {
|
||||
let resolutions = [
|
||||
None,
|
||||
Some(CommonResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
Some(CommonResolution::TenEightyP), // clamps to 720p on both sides
|
||||
];
|
||||
let durations: &[u16] = &[1, 4, 5, 8, 10, 12, 15];
|
||||
let aspect_ratios = [
|
||||
None,
|
||||
Some(CommonAspectRatio::Auto),
|
||||
Some(CommonAspectRatio::Square),
|
||||
Some(CommonAspectRatio::WideSixteenByNine),
|
||||
Some(CommonAspectRatio::TallNineBySixteen),
|
||||
Some(CommonAspectRatio::WideFourByThree),
|
||||
Some(CommonAspectRatio::TallThreeByFour),
|
||||
Some(CommonAspectRatio::WideThreeByTwo),
|
||||
Some(CommonAspectRatio::TallTwoByThree),
|
||||
];
|
||||
// (has_start_frame, num_reference_images). At most one of the two is
|
||||
// non-default — start_frame and reference_images are mutually exclusive
|
||||
// on the GrokApi wire (image-to-video vs reference-to-video), so we
|
||||
// don't mix them here to keep the comparison apples-to-apples.
|
||||
let image_combos: &[(bool, usize)] = &[
|
||||
(false, 0), // text-to-video
|
||||
(true, 0), // image-to-video, 1 source image
|
||||
(false, 1), // reference-to-video, 1 ref image
|
||||
(false, 2),
|
||||
(false, 3),
|
||||
];
|
||||
|
||||
let mut total_cases = 0;
|
||||
let mut failures = Vec::new();
|
||||
|
||||
for &res in &resolutions {
|
||||
for &dur in durations {
|
||||
for &ar in &aspect_ratios {
|
||||
for &(has_start, num_refs) in image_combos {
|
||||
total_cases += 1;
|
||||
let artcraft = artcraft_cents(res, dur, ar, has_start, num_refs);
|
||||
let grokapi = grokapi_cents(res, dur, ar, has_start, num_refs);
|
||||
|
||||
// Margin requirement: artcraft / max(grokapi - 1, 1) >= 1.29.
|
||||
// The (-1, 1) floor accounts for one cent of GrokApi-side
|
||||
// ceiling-rounding noise (see test doc comment above).
|
||||
let grokapi_compare = grokapi.saturating_sub(1).max(1);
|
||||
let lhs = artcraft.saturating_mul(100);
|
||||
let rhs = grokapi_compare.saturating_mul(129);
|
||||
|
||||
if lhs < rhs {
|
||||
let pct = if grokapi == 0 { 0.0 }
|
||||
else { (artcraft as f64 / grokapi as f64 - 1.0) * 100.0 };
|
||||
failures.push(format!(
|
||||
"res={:?} dur={}s ar={:?} start={} refs={} → \
|
||||
artcraft={}¢ vs grokapi={}¢ (margin {:.1}%, want ≥ 29% after 1¢ tolerance)",
|
||||
res, dur, ar, has_start, num_refs, artcraft, grokapi, pct,
|
||||
));
|
||||
}
|
||||
|
||||
// Defense in depth: ArtCraft must always cost at least as
|
||||
// much as GrokApi, regardless of rounding noise.
|
||||
assert!(
|
||||
artcraft >= grokapi,
|
||||
"ArtCraft ({artcraft}¢) cheaper than GrokApi ({grokapi}¢) for \
|
||||
res={res:?} dur={dur}s ar={ar:?} start={has_start} refs={num_refs}",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
let summary = failures.iter().take(10).cloned().collect::<Vec<_>>().join("\n ");
|
||||
panic!(
|
||||
"{} / {} (res × duration × aspect × image_combo) tuples failed \
|
||||
the ≥29% (with 1¢ tolerance) margin check.\nFirst 10:\n {}",
|
||||
failures.len(), total_cases, summary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Cross-provider helpers ──
|
||||
|
||||
fn artcraft_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
has_start_frame: bool,
|
||||
num_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
// ArtCraft's omni endpoint takes MediaFileTokens for image refs.
|
||||
Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())))
|
||||
} else { None };
|
||||
let reference_images = if num_reference_images > 0 {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(0..num_reference_images)
|
||||
.map(|i| MediaFileToken::new(format!("mf_ref_{i}")))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
// Batch is fixed at 1 because GrokApi's per-request cost calculator
|
||||
// doesn't multiply by batch (Grok returns one video per call); the
|
||||
// markup test would compare apples to oranges otherwise.
|
||||
video_batch_count: Some(1),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 (artcraft) should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost (artcraft) should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn grokapi_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
has_start_frame: bool,
|
||||
num_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
// GrokApi (direct xAI) takes URLs for image refs.
|
||||
Some(ImageRef::Url("https://example.com/start.png".to_string()))
|
||||
} else { None };
|
||||
let reference_images = if num_reference_images > 0 {
|
||||
Some(ImageListRef::Urls(
|
||||
(0..num_reference_images)
|
||||
.map(|i| format!("https://example.com/ref_{i}.png"))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::GrokApi,
|
||||
resolution,
|
||||
aspect_ratio,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(1),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 (grokapi) should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost (grokapi) should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn build_cost(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> VideoGenerationCostEstimate {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
) -> u64 {
|
||||
build_cost(resolution, duration_seconds, video_batch_count)
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn cost_cents_with_images(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
video_batch_count: u16,
|
||||
has_start_frame: bool,
|
||||
extra_reference_images: usize,
|
||||
) -> u64 {
|
||||
let start_frame = if has_start_frame {
|
||||
Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_start".to_string())))
|
||||
} else { None };
|
||||
let reference_images = if extra_reference_images > 0 {
|
||||
Some(ImageListRef::MediaFileTokens(
|
||||
(0..extra_reference_images)
|
||||
.map(|i| MediaFileToken::new(format!("mf_ref_{i}")))
|
||||
.collect(),
|
||||
))
|
||||
} else { None };
|
||||
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::Artcraft,
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(video_batch_count),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
|
||||
|
||||
use crate::client::router_artcraft_client::RouterArtcraftClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::request_common::send_artcraft_omni_request;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtcraftGrokImagineVideoRequestState {
|
||||
pub request: OmniGenVideoCostAndGenerateRequest,
|
||||
}
|
||||
|
||||
impl ArtcraftGrokImagineVideoRequestState {
|
||||
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
send_artcraft_omni_request(&self.request, client).await
|
||||
}
|
||||
}
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
pub(crate) mod build_common;
|
||||
pub mod grok_imagine_video;
|
||||
pub mod happy_horse_1p0;
|
||||
pub mod preview_model;
|
||||
pub mod preview_model_fast;
|
||||
|
||||
+553
@@ -0,0 +1,553 @@
|
||||
use log::info;
|
||||
|
||||
use grok_api_client::api::requests::videos::video_generation::video_generation::{
|
||||
VideoGenerationRequest as GrokVideoGenerationRequest,
|
||||
VideoImageSource as GrokVideoImageSource,
|
||||
};
|
||||
use grok_api_client::api::types::video_types::video_aspect_ratio::VideoAspectRatio as GrokAspectRatio;
|
||||
use grok_api_client::api::types::video_types::video_resolution::VideoResolution as GrokResolution;
|
||||
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::client::request_mismatch_mitigation_strategy::RequestMismatchMitigationStrategy;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::request::GrokApiGrokImagineVideoRequestState;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
/// Builds a Grok Imagine Video request from the generic GenerateVideoRequestBuilder.
|
||||
///
|
||||
/// xAI's grok-imagine-video accepts:
|
||||
/// - `image` (single source image, image-to-video mode) OR `reference_images` (multi-image
|
||||
/// reference-to-video). These two are mutually exclusive per xAI's API.
|
||||
/// - Aspect ratio, resolution (480p/720p), duration (1–15s), prompt.
|
||||
///
|
||||
/// Fields that Grok DOESN'T accept (`end_frame`, `reference_videos`, `reference_audio`,
|
||||
/// and `MediaFileToken`-style image refs) are silently dropped with an info-level log.
|
||||
/// Same goes for the `start_frame` + `reference_images` conflict — we prefer
|
||||
/// `start_frame` (image-to-video) and drop `reference_images`. This keeps the
|
||||
/// pipeline tolerant when callers fan a single builder out to multiple providers
|
||||
/// with different capabilities.
|
||||
pub fn build_grok_api_grok_imagine_video(
|
||||
mut builder: GenerateVideoRequestBuilder,
|
||||
) -> Result<VideoGenerationDraftOrRequest, ArtcraftRouterError> {
|
||||
let strategy = builder.request_mismatch_mitigation_strategy;
|
||||
|
||||
// Scalar fields.
|
||||
let prompt = builder.prompt.take().unwrap_or_default();
|
||||
let aspect_ratio = plan_aspect_ratio(builder.aspect_ratio.take(), strategy);
|
||||
let resolution = plan_resolution(builder.resolution.take(), strategy);
|
||||
let duration = builder.duration_seconds.take().map(|d| (d as u32).clamp(1, 15));
|
||||
|
||||
// Wholly-unsupported fields → drop with a log so it shows up in operator traces.
|
||||
log_and_drop_end_frame(builder.end_frame.take());
|
||||
log_and_drop_reference_videos(builder.reference_videos.take());
|
||||
log_and_drop_reference_audio(builder.reference_audio.take());
|
||||
|
||||
// Image sources: prefer start_frame (image-to-video). If both start_frame and
|
||||
// reference_images are supplied, drop reference_images (xAI rejects the
|
||||
// combination on the wire).
|
||||
let start_frame = resolve_url_to_image_source(builder.start_frame.take());
|
||||
let reference_images_supplied = builder.reference_images.take();
|
||||
|
||||
let (image, reference_images) = match (start_frame, reference_images_supplied) {
|
||||
(Some(img), Some(refs)) => {
|
||||
info!(
|
||||
"grok_imagine_video: both start_frame and reference_images supplied — \
|
||||
preferring start_frame (image-to-video). Dropping {} reference images.",
|
||||
count_image_list_ref(&refs),
|
||||
);
|
||||
(Some(img), None)
|
||||
}
|
||||
(Some(img), None) => (Some(img), None),
|
||||
(None, Some(refs)) => (None, resolve_url_list_to_image_sources(refs)),
|
||||
(None, None) => (None, None),
|
||||
};
|
||||
|
||||
let request = GrokVideoGenerationRequest {
|
||||
prompt,
|
||||
model: None, // defaults to grok-imagine-video in the client
|
||||
image,
|
||||
reference_images,
|
||||
aspect_ratio,
|
||||
duration,
|
||||
resolution,
|
||||
user: None,
|
||||
};
|
||||
|
||||
let state = GrokApiGrokImagineVideoRequestState { request };
|
||||
Ok(VideoGenerationDraftOrRequest::Request(
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo(state),
|
||||
))
|
||||
}
|
||||
|
||||
// ── Field planners ──
|
||||
|
||||
fn plan_aspect_ratio(
|
||||
aspect_ratio: Option<CommonAspectRatio>,
|
||||
_strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Option<GrokAspectRatio> {
|
||||
// xAI supports exactly: 1:1, 16:9, 9:16, 4:3, 3:4, 3:2, 2:3.
|
||||
// Auto / unsupported ratios fall back to the closest match (or None → xAI default 16:9).
|
||||
match aspect_ratio {
|
||||
None
|
||||
| Some(CommonAspectRatio::Auto)
|
||||
| Some(CommonAspectRatio::Auto2k)
|
||||
| Some(CommonAspectRatio::Auto3k)
|
||||
| Some(CommonAspectRatio::Auto4k) => None,
|
||||
|
||||
Some(CommonAspectRatio::Square) | Some(CommonAspectRatio::SquareHd) => {
|
||||
Some(GrokAspectRatio::Square)
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::WideSixteenByNine) | Some(CommonAspectRatio::Wide) => {
|
||||
Some(GrokAspectRatio::Landscape16x9)
|
||||
}
|
||||
Some(CommonAspectRatio::TallNineBySixteen) | Some(CommonAspectRatio::Tall) => {
|
||||
Some(GrokAspectRatio::Portrait9x16)
|
||||
}
|
||||
|
||||
Some(CommonAspectRatio::WideFourByThree) => Some(GrokAspectRatio::Landscape4x3),
|
||||
Some(CommonAspectRatio::TallThreeByFour) => Some(GrokAspectRatio::Portrait3x4),
|
||||
|
||||
Some(CommonAspectRatio::WideThreeByTwo) => Some(GrokAspectRatio::Landscape3x2),
|
||||
Some(CommonAspectRatio::TallTwoByThree) => Some(GrokAspectRatio::Portrait2x3),
|
||||
|
||||
// No exact xAI match — pick the closest cardinal direction.
|
||||
Some(CommonAspectRatio::WideFiveByFour)
|
||||
| Some(CommonAspectRatio::WideTwentyOneByNine) => Some(GrokAspectRatio::Landscape16x9),
|
||||
Some(CommonAspectRatio::TallFourByFive)
|
||||
| Some(CommonAspectRatio::TallNineByTwentyOne) => Some(GrokAspectRatio::Portrait9x16),
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_resolution(
|
||||
resolution: Option<CommonResolution>,
|
||||
_strategy: RequestMismatchMitigationStrategy,
|
||||
) -> Option<GrokResolution> {
|
||||
// Grok supports 480p and 720p only (1080p is downsized to 720p per xAI docs).
|
||||
match resolution {
|
||||
None => None,
|
||||
Some(CommonResolution::FourEightyP) => Some(GrokResolution::FourEightyP),
|
||||
Some(CommonResolution::SevenTwentyP) => Some(GrokResolution::SevenTwentyP),
|
||||
// Higher-than-720p requests get clamped to 720p (Grok's cap).
|
||||
Some(CommonResolution::TenEightyP)
|
||||
| Some(CommonResolution::TwoK)
|
||||
| Some(CommonResolution::ThreeK)
|
||||
| Some(CommonResolution::FourK) => Some(GrokResolution::SevenTwentyP),
|
||||
// Lower-than-480p requests get bumped to 480p (Grok's floor).
|
||||
Some(CommonResolution::HalfK) | Some(CommonResolution::OneK) => {
|
||||
Some(GrokResolution::FourEightyP)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image source resolvers (silent drops for unsupported variants) ──
|
||||
|
||||
fn resolve_url_to_image_source(image_ref: Option<ImageRef>) -> Option<GrokVideoImageSource> {
|
||||
match image_ref {
|
||||
None => None,
|
||||
Some(ImageRef::Url(url)) => Some(GrokVideoImageSource::Url(url)),
|
||||
Some(ImageRef::MediaFileToken(token)) => {
|
||||
info!(
|
||||
"grok_imagine_video: dropping MediaFileToken {:?} for start_frame — \
|
||||
Grok only accepts public URLs. Resolve the token to a URL upstream if needed.",
|
||||
token,
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_url_list_to_image_sources(list_ref: ImageListRef) -> Option<Vec<GrokVideoImageSource>> {
|
||||
match list_ref {
|
||||
ImageListRef::Urls(urls) if urls.is_empty() => None,
|
||||
ImageListRef::Urls(urls) => Some(urls.into_iter().map(GrokVideoImageSource::Url).collect()),
|
||||
ImageListRef::MediaFileTokens(tokens) => {
|
||||
info!(
|
||||
"grok_imagine_video: dropping {} MediaFileToken reference image(s) — \
|
||||
Grok only accepts public URLs. Resolve tokens to URLs upstream if needed.",
|
||||
tokens.len(),
|
||||
);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn count_image_list_ref(refs: &ImageListRef) -> usize {
|
||||
match refs {
|
||||
ImageListRef::Urls(v) => v.len(),
|
||||
ImageListRef::MediaFileTokens(v) => v.len(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Drop-with-log helpers for fields Grok doesn't accept at all ──
|
||||
|
||||
fn log_and_drop_end_frame(end_frame: Option<ImageRef>) {
|
||||
if end_frame.is_some() {
|
||||
info!("grok_imagine_video: dropping end_frame — Grok doesn't support end-frame keyframes.");
|
||||
}
|
||||
}
|
||||
|
||||
fn log_and_drop_reference_videos(refs: Option<VideoListRef>) {
|
||||
if let Some(refs) = refs {
|
||||
let count = match refs {
|
||||
VideoListRef::Urls(v) => v.len(),
|
||||
VideoListRef::MediaFileTokens(v) => v.len(),
|
||||
};
|
||||
info!(
|
||||
"grok_imagine_video: dropping {} reference video(s) — Grok doesn't accept reference videos.",
|
||||
count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn log_and_drop_reference_audio(refs: Option<AudioListRef>) {
|
||||
if let Some(refs) = refs {
|
||||
let count = match refs {
|
||||
AudioListRef::Urls(v) => v.len(),
|
||||
AudioListRef::MediaFileTokens(v) => v.len(),
|
||||
};
|
||||
info!(
|
||||
"grok_imagine_video: dropping {} reference audio clip(s) — Grok doesn't accept reference audio.",
|
||||
count,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokens::tokens::media_files::MediaFileToken;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::api::video_list_ref::VideoListRef;
|
||||
use crate::api::audio_list_ref::AudioListRef;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
use crate::generate::generate_video_v2::video_generation_request::VideoGenerationRequest;
|
||||
|
||||
// ── Field passthrough ──
|
||||
|
||||
mod field_conversions {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn prompt_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.prompt = Some("test prompt".to_string()); }));
|
||||
assert_eq!(req.request.prompt, "test prompt");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_passed_through() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(8); }));
|
||||
assert_eq!(req.request.duration, Some(8));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_min() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(0); }));
|
||||
assert_eq!(req.request.duration, Some(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_clamped_to_max() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = Some(99); }));
|
||||
assert_eq!(req.request.duration, Some(15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn duration_none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|b| { b.duration_seconds = None; }));
|
||||
assert_eq!(req.request.duration, None);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Aspect ratio ──
|
||||
|
||||
mod aspect_ratio_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn square() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::Square); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Square));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_16x9() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideSixteenByNine); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_9x16() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::TallNineBySixteen); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait9x16));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_4x3() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideFourByThree); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape4x3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_3x4() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::TallThreeByFour); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait3x4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn landscape_3x2() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::WideThreeByTwo); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape3x2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn portrait_2x3() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::TallTwoByThree); }));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait2x3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_maps_to_none() {
|
||||
let req = unwrap_request(make_builder(|b| { b.aspect_ratio = Some(CommonAspectRatio::Auto); }));
|
||||
assert_eq!(req.request.aspect_ratio, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_wide_falls_back_to_16x9() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::WideTwentyOneByNine);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_tall_falls_back_to_9x16() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.aspect_ratio = Some(CommonAspectRatio::TallNineByTwentyOne);
|
||||
}));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Portrait9x16));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Resolution ──
|
||||
|
||||
mod resolution_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn res_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::SevenTwentyP); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1080p_clamps_to_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::TenEightyP); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_4k_clamps_to_720p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::FourK); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn res_1k_bumps_to_480p() {
|
||||
let req = unwrap_request(make_builder(|b| { b.resolution = Some(CommonResolution::OneK); }));
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::FourEightyP));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn none_stays_none() {
|
||||
let req = unwrap_request(make_builder(|_| {}));
|
||||
assert!(req.request.resolution.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Image source plumbing (graceful — never errors) ──
|
||||
|
||||
mod image_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn start_frame_url_becomes_image() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
}));
|
||||
match &req.request.image {
|
||||
Some(GrokVideoImageSource::Url(u)) => assert_eq!(u, "https://example.com/start.png"),
|
||||
other => panic!("expected Url variant, got {:?}", other),
|
||||
}
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_media_file_token_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_test".to_string())));
|
||||
}));
|
||||
// Dropped, request still builds successfully with no image.
|
||||
assert!(req.request.image.is_none());
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_urls_passed_through() {
|
||||
let urls = vec!["https://example.com/a.png".to_string(), "https://example.com/b.png".to_string()];
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::Urls(urls.clone()));
|
||||
}));
|
||||
let refs = req.request.reference_images.expect("reference_images should be set");
|
||||
assert_eq!(refs.len(), 2);
|
||||
assert!(req.request.image.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_image_tokens_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::MediaFileTokens(vec![
|
||||
MediaFileToken::new("mf_a".to_string()),
|
||||
MediaFileToken::new("mf_b".to_string()),
|
||||
]));
|
||||
}));
|
||||
// All tokens dropped; reference_images becomes None.
|
||||
assert!(req.request.reference_images.is_none());
|
||||
assert!(req.request.image.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn start_frame_and_reference_images_together_prefers_start_frame() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/ref1.png".to_string(),
|
||||
"https://example.com/ref2.png".to_string(),
|
||||
]));
|
||||
}));
|
||||
// start_frame wins; reference_images dropped (mutually exclusive on the wire).
|
||||
match &req.request.image {
|
||||
Some(GrokVideoImageSource::Url(u)) => assert_eq!(u, "https://example.com/start.png"),
|
||||
other => panic!("expected start_frame URL, got {:?}", other),
|
||||
}
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_reference_image_url_list_becomes_none() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
// ── Unsupported fields are silently dropped, not rejected ──
|
||||
|
||||
mod unsupported_fields_graceful {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn end_frame_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string()));
|
||||
}));
|
||||
// Request still builds; Grok's request body has no end_frame field.
|
||||
assert!(req.request.image.is_none());
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_videos_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_videos = Some(VideoListRef::Urls(vec!["https://example.com/v.mp4".to_string()]));
|
||||
}));
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_audio_silently_dropped() {
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.reference_audio = Some(AudioListRef::Urls(vec!["https://example.com/a.wav".to_string()]));
|
||||
}));
|
||||
// No audio field on Grok; just gone.
|
||||
assert!(req.request.reference_images.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitchen_sink_with_every_unsupported_field_still_succeeds() {
|
||||
// Worst-case builder fanned out from a multi-provider context. Grok
|
||||
// should keep what it can and drop the rest without erroring.
|
||||
let req = unwrap_request(make_builder(|b| {
|
||||
b.prompt = Some("kitchen sink".to_string());
|
||||
b.start_frame = Some(ImageRef::Url("https://example.com/start.png".to_string()));
|
||||
b.end_frame = Some(ImageRef::Url("https://example.com/end.png".to_string()));
|
||||
b.reference_images = Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/ref.png".to_string(),
|
||||
]));
|
||||
b.reference_videos = Some(VideoListRef::Urls(vec!["https://example.com/v.mp4".to_string()]));
|
||||
b.reference_audio = Some(AudioListRef::Urls(vec!["https://example.com/a.wav".to_string()]));
|
||||
b.resolution = Some(CommonResolution::SevenTwentyP);
|
||||
b.aspect_ratio = Some(CommonAspectRatio::WideSixteenByNine);
|
||||
b.duration_seconds = Some(8);
|
||||
}));
|
||||
// Kept: prompt + start_frame + resolution + aspect_ratio + duration.
|
||||
assert_eq!(req.request.prompt, "kitchen sink");
|
||||
assert!(matches!(
|
||||
req.request.image,
|
||||
Some(GrokVideoImageSource::Url(_))
|
||||
));
|
||||
// Dropped because start_frame won the image-source contest.
|
||||
assert!(req.request.reference_images.is_none());
|
||||
assert_eq!(req.request.resolution, Some(GrokResolution::SevenTwentyP));
|
||||
assert_eq!(req.request.aspect_ratio, Some(GrokAspectRatio::Landscape16x9));
|
||||
assert_eq!(req.request.duration, Some(8));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──
|
||||
|
||||
fn base_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::GrokApi,
|
||||
duration_seconds: Some(5),
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
|
||||
let mut b = base_builder();
|
||||
f(&mut b);
|
||||
b
|
||||
}
|
||||
|
||||
fn unwrap_request(builder: GenerateVideoRequestBuilder) -> GrokApiGrokImagineVideoRequestState {
|
||||
let result = build_grok_api_grok_imagine_video(builder).expect("build should succeed");
|
||||
match result {
|
||||
VideoGenerationDraftOrRequest::Request(VideoGenerationRequest::GrokApiGrokImagineVideo(s)) => s,
|
||||
_ => panic!("expected GrokApiGrokImagineVideo request"),
|
||||
}
|
||||
}
|
||||
}
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
use grok_api_client::api::traits::grok_request_cost_calculator_trait::GrokRequestCostCalculator;
|
||||
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::request::GrokApiGrokImagineVideoRequestState;
|
||||
|
||||
pub struct GrokApiGrokImagineVideoCostState {
|
||||
request: GrokApiGrokImagineVideoRequestState,
|
||||
}
|
||||
|
||||
impl GrokApiGrokImagineVideoCostState {
|
||||
pub fn from_request(request: &GrokApiGrokImagineVideoRequestState) -> Self {
|
||||
Self { request: request.clone() }
|
||||
}
|
||||
|
||||
pub fn estimate_cost(&self) -> VideoGenerationCostEstimate {
|
||||
// grok_api_client's GrokRequestCostCalculator does the math:
|
||||
// - Output: resolution × duration (50 mills/s @ 480p, 70 mills/s @ 720p)
|
||||
// - Input: 2 mills × source image count
|
||||
// No source-video duration to factor in for `video_generation` — it's
|
||||
// text/image → video, not edit/extension.
|
||||
let cost_in_usd_cents = self.request.request.calculate_cost_in_cents();
|
||||
|
||||
VideoGenerationCostEstimate {
|
||||
cost_in_credits: None,
|
||||
cost_in_usd_cents: Some(cost_in_usd_cents),
|
||||
is_free: false,
|
||||
is_unlimited: false,
|
||||
is_rate_limited: false,
|
||||
has_watermark: false,
|
||||
failures_are_refunded: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_list_ref::ImageListRef;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
|
||||
// Pricing reference (from grok_api_client/cost.rs, mirrors xAI's table):
|
||||
// Output: 480p 50 mills/s ($0.05/s)
|
||||
// 720p 70 mills/s ($0.07/s)
|
||||
// Input: 2 mills per source image ($0.002/img)
|
||||
//
|
||||
// Cost in cents = ceil(mills / 10).
|
||||
|
||||
mod pricing_720p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn five_s_no_image_ref() {
|
||||
// 70 × 5 = 350 mills → 35¢
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 5, None, None), 35);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_s_no_image_ref() {
|
||||
// 70 × 10 = 700 mills → 70¢
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 10, None, None), 70);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifteen_s_no_image_ref() {
|
||||
// 70 × 15 = 1050 mills → 105¢
|
||||
assert_eq!(cost_cents(Some(CommonResolution::SevenTwentyP), 15, None, None), 105);
|
||||
}
|
||||
}
|
||||
|
||||
mod pricing_480p {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn five_s_no_image_ref() {
|
||||
// 50 × 5 = 250 mills → 25¢
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 5, None, None), 25);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_s_no_image_ref() {
|
||||
// 50 × 10 = 500 mills → 50¢
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 10, None, None), 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fifteen_s_no_image_ref() {
|
||||
// 50 × 15 = 750 mills → 75¢
|
||||
assert_eq!(cost_cents(Some(CommonResolution::FourEightyP), 15, None, None), 75);
|
||||
}
|
||||
}
|
||||
|
||||
mod image_inputs_add_to_cost {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn single_start_frame_image_adds_2_mills() {
|
||||
// 5s @ 720p output 350 mills + 1 input image 2 mills = 352 mills → 36¢ (ceil)
|
||||
let cents = cost_cents(
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
5,
|
||||
Some(ImageRef::Url("https://example.com/a.png".to_string())),
|
||||
None,
|
||||
);
|
||||
assert_eq!(cents, 36);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_reference_images_adds_6_mills() {
|
||||
// 5s @ 720p output 350 mills + 3 input images 6 mills = 356 mills → 36¢ (ceil)
|
||||
let cents = cost_cents(
|
||||
Some(CommonResolution::SevenTwentyP),
|
||||
5,
|
||||
None,
|
||||
Some(ImageListRef::Urls(vec![
|
||||
"https://example.com/a.png".to_string(),
|
||||
"https://example.com/b.png".to_string(),
|
||||
"https://example.com/c.png".to_string(),
|
||||
])),
|
||||
);
|
||||
assert_eq!(cents, 36);
|
||||
}
|
||||
}
|
||||
|
||||
mod relative_pricing {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn higher_resolution_costs_more() {
|
||||
let p480 = cost_cents(Some(CommonResolution::FourEightyP), 10, None, None);
|
||||
let p720 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, None, None);
|
||||
assert!(p480 < p720, "480p ({p480}) should be < 720p ({p720})");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn longer_duration_costs_more() {
|
||||
let c5 = cost_cents(Some(CommonResolution::SevenTwentyP), 5, None, None);
|
||||
let c10 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, None, None);
|
||||
let c15 = cost_cents(Some(CommonResolution::SevenTwentyP), 15, None, None);
|
||||
assert!(c5 < c10);
|
||||
assert!(c10 < c15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn one_k_request_clamps_to_480p_pricing() {
|
||||
// OneK gets bumped to 480p in plan_resolution, so cost equals 480p cost.
|
||||
let one_k = cost_cents(Some(CommonResolution::OneK), 10, None, None);
|
||||
let p480 = cost_cents(Some(CommonResolution::FourEightyP), 10, None, None);
|
||||
assert_eq!(one_k, p480);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ten_eighty_p_request_clamps_to_720p_pricing() {
|
||||
// TenEightyP gets clamped to 720p in plan_resolution.
|
||||
let p1080 = cost_cents(Some(CommonResolution::TenEightyP), 10, None, None);
|
||||
let p720 = cost_cents(Some(CommonResolution::SevenTwentyP), 10, None, None);
|
||||
assert_eq!(p1080, p720);
|
||||
}
|
||||
}
|
||||
|
||||
fn cost_cents(
|
||||
resolution: Option<CommonResolution>,
|
||||
duration_seconds: u16,
|
||||
start_frame: Option<ImageRef>,
|
||||
reference_images: Option<ImageListRef>,
|
||||
) -> u64 {
|
||||
let builder = GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::GrokApi,
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution,
|
||||
duration_seconds: Some(duration_seconds),
|
||||
video_batch_count: Some(1),
|
||||
start_frame,
|
||||
reference_images,
|
||||
..Default::default()
|
||||
};
|
||||
builder.build2()
|
||||
.expect("build2 should succeed")
|
||||
.estimate_cost()
|
||||
.expect("estimate_cost should succeed")
|
||||
.cost_in_usd_cents
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
pub mod build;
|
||||
pub mod cost;
|
||||
pub mod request;
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use grok_api_client::api::requests::videos::video_generation::video_generation::{
|
||||
video_generation, VideoGenerationArgs, VideoGenerationRequest as GrokVideoGenerationRequest,
|
||||
};
|
||||
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::errors::provider_error::ProviderError;
|
||||
use crate::generate::generate_video::generate_video_response::{
|
||||
GenerateVideoResponse, GrokVideoResponsePayload,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GrokApiGrokImagineVideoRequestState {
|
||||
/// The fully-resolved Grok request body. Doesn't carry the API key — that
|
||||
/// gets borrowed at send time from the router client.
|
||||
pub request: GrokVideoGenerationRequest,
|
||||
}
|
||||
|
||||
impl GrokApiGrokImagineVideoRequestState {
|
||||
pub async fn send(&self, client: &RouterGrokApiClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
let outbound_request = Arc::new(self.request.clone());
|
||||
|
||||
let response = video_generation(VideoGenerationArgs {
|
||||
api_key: &client.api_key,
|
||||
request: self.request.clone(),
|
||||
})
|
||||
.await
|
||||
.map_err(|err| ArtcraftRouterError::Provider(ProviderError::Grok(err)))?;
|
||||
|
||||
Ok(GenerateVideoResponse::Grok(GrokVideoResponsePayload {
|
||||
request_id: response.request_id,
|
||||
maybe_outbound_request: Some(outbound_request),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use grok_api_client::creds::grok_api_key::GrokApiKey;
|
||||
use test_data::web::image_urls::JUNO_AT_LAKE_IMAGE_URL;
|
||||
|
||||
use crate::api::common_aspect_ratio::CommonAspectRatio;
|
||||
use crate::api::common_resolution::CommonResolution;
|
||||
use crate::api::common_video_model::CommonVideoModel;
|
||||
use crate::api::image_ref::ImageRef;
|
||||
use crate::api::provider::Provider;
|
||||
use crate::client::router_client::RouterClient;
|
||||
use crate::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video_v2::video_generation_draft_or_request::VideoGenerationDraftOrRequest;
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs costs
|
||||
async fn test_text_to_video_720p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("A glowing crystal rocket launching from Mars.".to_string()),
|
||||
aspect_ratio: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution: Some(CommonResolution::SevenTwentyP),
|
||||
duration_seconds: Some(5),
|
||||
..grok_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Grok(_)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore] // requires real API key, incurs costs
|
||||
async fn test_image_to_video_480p() {
|
||||
let response = run_pipeline(GenerateVideoRequestBuilder {
|
||||
prompt: Some("The dog leaps into the lake and splashes around.".to_string()),
|
||||
start_frame: Some(ImageRef::Url(JUNO_AT_LAKE_IMAGE_URL.to_string())),
|
||||
resolution: Some(CommonResolution::FourEightyP),
|
||||
duration_seconds: Some(5),
|
||||
..grok_builder()
|
||||
}).await;
|
||||
assert!(matches!(response, GenerateVideoResponse::Grok(_)));
|
||||
}
|
||||
|
||||
fn grok_builder() -> GenerateVideoRequestBuilder {
|
||||
GenerateVideoRequestBuilder {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
provider: Provider::GrokApi,
|
||||
video_batch_count: Some(1),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_grok_client() -> RouterClient {
|
||||
let secret = std::fs::read_to_string("/Users/bt/Artcraft/credentials/grok_api_key.txt")
|
||||
.expect("Failed to read Grok API key");
|
||||
let api_key = GrokApiKey::new(secret.trim().to_string());
|
||||
RouterClient::GrokApi(RouterGrokApiClient::new(api_key))
|
||||
}
|
||||
|
||||
async fn run_pipeline(builder: GenerateVideoRequestBuilder) -> GenerateVideoResponse {
|
||||
let client = get_grok_client();
|
||||
let draft_or_request = builder.build2().expect("build2 should succeed");
|
||||
let request = match draft_or_request {
|
||||
VideoGenerationDraftOrRequest::Request(r) => r,
|
||||
_ => panic!("expected Request variant (Grok skips draft)"),
|
||||
};
|
||||
let response = request.send_request(&client).await.expect("send_request should succeed");
|
||||
match &response {
|
||||
GenerateVideoResponse::Grok(p) => println!("grok request_id={}", p.request_id),
|
||||
other => println!("unexpected response: {:?}", other),
|
||||
}
|
||||
response
|
||||
}
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
pub mod grok_imagine_video;
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
pub mod artcraft;
|
||||
pub mod gmicloud;
|
||||
pub mod grok_api;
|
||||
pub mod kinovi;
|
||||
+18
@@ -3,6 +3,8 @@ use crate::client::router_client::RouterClient;
|
||||
use crate::errors::artcraft_router_error::ArtcraftRouterError;
|
||||
use crate::generate::generate_video::generate_video_response::GenerateVideoResponse;
|
||||
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::cost::ArtcraftGrokImagineVideoCostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::grok_imagine_video::request::ArtcraftGrokImagineVideoRequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::cost::ArtcraftHappyHorse1p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::happy_horse_1p0::request::ArtcraftHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::artcraft::preview_model::cost::ArtcraftPreviewModelCostState;
|
||||
@@ -25,6 +27,8 @@ use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::cos
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_g::request::GmiCloudSeedance2p0UltraRequestState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::cost::GmiCloudSeedance2p0UltraFastCostState;
|
||||
use crate::generate::generate_video_v2::providers::gmicloud::seedance_2p0_fast_g::request::GmiCloudSeedance2p0UltraFastRequestState;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::cost::GrokApiGrokImagineVideoCostState;
|
||||
use crate::generate::generate_video_v2::providers::grok_api::grok_imagine_video::request::GrokApiGrokImagineVideoRequestState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::cost::KinoviHappyHorse1p0CostState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::happy_horse_1p0::request::KinoviHappyHorse1p0RequestState;
|
||||
use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0::cost::KinoviSeedance2p0CostState;
|
||||
@@ -34,6 +38,7 @@ use crate::generate::generate_video_v2::providers::kinovi::seedance_2p0_fast::re
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum VideoGenerationRequest {
|
||||
ArtcraftGrokImagineVideo(ArtcraftGrokImagineVideoRequestState),
|
||||
ArtcraftHappyHorse1p0(ArtcraftHappyHorse1p0RequestState),
|
||||
ArtcraftPreviewModel(ArtcraftPreviewModelRequestState),
|
||||
ArtcraftPreviewModelFast(ArtcraftPreviewModelFastRequestState),
|
||||
@@ -45,6 +50,7 @@ pub enum VideoGenerationRequest {
|
||||
ArtcraftSeedance2p0BytePlusFast(ArtcraftSeedance2p0BytePlusFastRequestState),
|
||||
GmiCloudSeedance2p0Ultra(GmiCloudSeedance2p0UltraRequestState),
|
||||
GmiCloudSeedance2p0UltraFast(GmiCloudSeedance2p0UltraFastRequestState),
|
||||
GrokApiGrokImagineVideo(GrokApiGrokImagineVideoRequestState),
|
||||
KinoviHappyHorse1p0(KinoviHappyHorse1p0RequestState),
|
||||
KinoviSeedance2p0(KinoviSeedance2p0RequestState),
|
||||
KinoviSeedance2p0Fast(KinoviSeedance2p0FastRequestState),
|
||||
@@ -54,6 +60,7 @@ impl VideoGenerationRequest {
|
||||
|
||||
pub fn get_provider(&self) -> Provider {
|
||||
match self {
|
||||
Self::ArtcraftGrokImagineVideo(_) => Provider::Artcraft,
|
||||
Self::ArtcraftHappyHorse1p0(_) => Provider::Artcraft,
|
||||
Self::ArtcraftPreviewModel(_) => Provider::Artcraft,
|
||||
Self::ArtcraftPreviewModelFast(_) => Provider::Artcraft,
|
||||
@@ -65,6 +72,7 @@ impl VideoGenerationRequest {
|
||||
Self::ArtcraftSeedance2p0BytePlusFast(_) => Provider::Artcraft,
|
||||
Self::GmiCloudSeedance2p0Ultra(_) => Provider::GmiCloud,
|
||||
Self::GmiCloudSeedance2p0UltraFast(_) => Provider::GmiCloud,
|
||||
Self::GrokApiGrokImagineVideo(_) => Provider::GrokApi,
|
||||
Self::KinoviHappyHorse1p0(_) => Provider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0(_) => Provider::Seedance2Pro,
|
||||
Self::KinoviSeedance2p0Fast(_) => Provider::Seedance2Pro,
|
||||
@@ -74,6 +82,7 @@ impl VideoGenerationRequest {
|
||||
/// Return a cost estimate to fulfill the request.
|
||||
pub fn estimate_cost(&self) -> Result<VideoGenerationCostEstimate, ArtcraftRouterError> {
|
||||
match self {
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo(request) => Ok(ArtcraftGrokImagineVideoCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => Ok(ArtcraftHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftPreviewModel(request) => Ok(ArtcraftPreviewModelCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::ArtcraftPreviewModelFast(request) => Ok(ArtcraftPreviewModelFastCostState::from_request(request).estimate_cost()),
|
||||
@@ -85,6 +94,7 @@ impl VideoGenerationRequest {
|
||||
VideoGenerationRequest::ArtcraftSeedance2p0BytePlusFast(request) => Ok(ArtcraftSeedance2p0BytePlusFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0Ultra(request) => Ok(GmiCloudSeedance2p0UltraCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GmiCloudSeedance2p0UltraFast(request) => Ok(GmiCloudSeedance2p0UltraFastCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo(request) => Ok(GrokApiGrokImagineVideoCostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviHappyHorse1p0(request) => Ok(KinoviHappyHorse1p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0(request) => Ok(KinoviSeedance2p0CostState::from_request(request).estimate_cost()),
|
||||
VideoGenerationRequest::KinoviSeedance2p0Fast(request) => Ok(KinoviSeedance2p0FastCostState::from_request(request).estimate_cost()),
|
||||
@@ -95,6 +105,10 @@ impl VideoGenerationRequest {
|
||||
/// If successful, returns the job IDs.
|
||||
pub async fn send_request(&self, client: &RouterClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
|
||||
match self {
|
||||
VideoGenerationRequest::ArtcraftGrokImagineVideo(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::ArtcraftHappyHorse1p0(request) => {
|
||||
let client_ref = client.get_artcraft_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
@@ -139,6 +153,10 @@ impl VideoGenerationRequest {
|
||||
let client_ref = client.get_gmicloud_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::GrokApiGrokImagineVideo(request) => {
|
||||
let client_ref = client.get_grok_api_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
},
|
||||
VideoGenerationRequest::KinoviHappyHorse1p0(request) => {
|
||||
let client_ref = client.get_seedance2pro_client_ref()?;
|
||||
request.send(client_ref).await
|
||||
|
||||
+2
-1
@@ -347,7 +347,8 @@ mod tests {
|
||||
let result = video_status(VideoStatusArgs {
|
||||
api_key: &api_key,
|
||||
request: VideoStatusRequest {
|
||||
request_id: "ff94f941-62a7-9966-92da-6b84d9eedb50".to_string(),
|
||||
//request_id: "ff94f941-62a7-9966-92da-6b84d9eedb50".to_string(),
|
||||
request_id: "4eea215a-9d21-9261-be1a-324409ec22c5".to_string(),
|
||||
},
|
||||
}).await;
|
||||
|
||||
|
||||
+77
@@ -0,0 +1,77 @@
|
||||
use log::info;
|
||||
use sqlx::{Executor, MySql};
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_category::InferenceCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory;
|
||||
use enums::by_table::generic_inference_jobs::inference_job_type::InferenceJobType;
|
||||
use enums::common::generation::common_model_type::CommonModelType;
|
||||
use enums::common::job_status_plus::JobStatusPlus;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::non_unique::debug_logs_event_token::DebugLogEventToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
|
||||
use crate::errors::database_query_error::DatabaseQueryError;
|
||||
use crate::queries::generic_inference::api_providers::common::insert_generic_inference_job_for_provider::{
|
||||
insert_generic_inference_job_for_provider,
|
||||
InsertGenericInferenceJobForProviderArgs,
|
||||
};
|
||||
|
||||
pub struct InsertGrokApiInferenceJobArgs<'e, 'c, E>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
pub apriori_job_token: &'e InferenceJobToken,
|
||||
pub uuid_idempotency_token: &'e str,
|
||||
pub external_request_id: &'e str,
|
||||
pub maybe_model_type: Option<CommonModelType>,
|
||||
pub maybe_prompt_token: Option<&'e PromptToken>,
|
||||
pub maybe_creator_user_token: Option<&'e UserToken>,
|
||||
pub maybe_avt_token: Option<&'e AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: &'e str,
|
||||
pub maybe_debug_log_event_token: Option<&'e DebugLogEventToken>,
|
||||
pub mysql_executor: E,
|
||||
pub phantom: PhantomData<&'c E>,
|
||||
}
|
||||
|
||||
pub async fn insert_generic_inference_job_for_grok_api<'e, 'c: 'e, E>(
|
||||
args: InsertGrokApiInferenceJobArgs<'e, 'c, E>,
|
||||
) -> Result<InferenceJobToken, DatabaseQueryError>
|
||||
where
|
||||
E: 'e + Executor<'c, Database = MySql>,
|
||||
{
|
||||
let record_id = insert_generic_inference_job_for_provider(InsertGenericInferenceJobForProviderArgs {
|
||||
apriori_job_token: args.apriori_job_token,
|
||||
uuid_idempotency_token: args.uuid_idempotency_token,
|
||||
job_type: InferenceJobType::GrokApiQueue,
|
||||
external_third_party: InferenceJobExternalThirdParty::GrokApi,
|
||||
external_third_party_id: args.external_request_id,
|
||||
product_category: InferenceJobProductCategory::GrokApiVideo,
|
||||
inference_category: InferenceCategory::VideoGeneration,
|
||||
maybe_model_type: args.maybe_model_type,
|
||||
maybe_prompt_token: args.maybe_prompt_token,
|
||||
maybe_wallet_ledger_entry_token: None,
|
||||
maybe_inference_args: None,
|
||||
maybe_creator_user_token: args.maybe_creator_user_token,
|
||||
maybe_avt_token: args.maybe_avt_token,
|
||||
creator_ip_address: args.creator_ip_address,
|
||||
creator_set_visibility: Visibility::Public,
|
||||
maybe_debug_log_event_token: args.maybe_debug_log_event_token,
|
||||
maybe_frontend_failure_category: None,
|
||||
maybe_failure_reason: None,
|
||||
status: JobStatusPlus::Pending,
|
||||
mysql_executor: args.mysql_executor,
|
||||
phantom: args.phantom,
|
||||
}).await?;
|
||||
|
||||
info!(
|
||||
"Insert generic inference job for Grok (xAI) API: {} with record ID {}",
|
||||
args.apriori_job_token, record_id
|
||||
);
|
||||
|
||||
Ok(args.apriori_job_token.clone())
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
use log::warn;
|
||||
use sqlx::MySqlPool;
|
||||
|
||||
use enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty;
|
||||
use enums::common::visibility::Visibility;
|
||||
use tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken;
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
use tokens::tokens::prompts::PromptToken;
|
||||
use tokens::tokens::users::UserToken;
|
||||
use tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken;
|
||||
|
||||
/// A Grok (xAI) API job that is waiting for a result from the external API.
|
||||
#[derive(Debug)]
|
||||
pub struct PendingGrokApiJob {
|
||||
pub job_token: InferenceJobToken,
|
||||
|
||||
/// The external `request_id` from xAI's video generation endpoint.
|
||||
pub request_id: String,
|
||||
|
||||
pub maybe_creator_user_token: Option<UserToken>,
|
||||
pub maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
pub creator_ip_address: String,
|
||||
pub creator_set_visibility: Visibility,
|
||||
|
||||
pub maybe_prompt_token: Option<PromptToken>,
|
||||
|
||||
pub maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct RawRecord {
|
||||
job_token: InferenceJobToken,
|
||||
request_id: Option<String>,
|
||||
maybe_creator_user_token: Option<UserToken>,
|
||||
maybe_creator_anonymous_visitor_token: Option<AnonymousVisitorTrackingToken>,
|
||||
creator_ip_address: String,
|
||||
creator_set_visibility: Visibility,
|
||||
maybe_prompt_token: Option<PromptToken>,
|
||||
maybe_wallet_ledger_entry_token: Option<WalletLedgerEntryToken>,
|
||||
}
|
||||
|
||||
/// Returns all non-terminal Grok (xAI) API jobs that have an associated request_id.
|
||||
pub async fn list_pending_grok_api_jobs(pool: &MySqlPool) -> Result<Vec<PendingGrokApiJob>, sqlx::Error> {
|
||||
let records = sqlx::query_as!(
|
||||
RawRecord,
|
||||
r#"
|
||||
SELECT
|
||||
jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,
|
||||
jobs.maybe_external_third_party_id as `request_id`,
|
||||
jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,
|
||||
jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,
|
||||
jobs.creator_ip_address,
|
||||
jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,
|
||||
jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,
|
||||
jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`
|
||||
|
||||
FROM generic_inference_jobs as jobs
|
||||
|
||||
WHERE jobs.maybe_external_third_party = ?
|
||||
AND jobs.status NOT IN ('complete_success', 'complete_failure')
|
||||
AND jobs.maybe_external_third_party_id IS NOT NULL
|
||||
|
||||
LIMIT 25000
|
||||
"#,
|
||||
InferenceJobExternalThirdParty::GrokApi.to_str(),
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
let jobs = records
|
||||
.into_iter()
|
||||
.filter_map(|record| {
|
||||
let request_id = match record.request_id {
|
||||
Some(id) => id,
|
||||
None => {
|
||||
warn!("PendingGrokApiJob has no request_id, skipping");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
Some(PendingGrokApiJob {
|
||||
job_token: record.job_token,
|
||||
request_id,
|
||||
maybe_creator_user_token: record.maybe_creator_user_token,
|
||||
maybe_creator_anonymous_visitor_token: record.maybe_creator_anonymous_visitor_token,
|
||||
creator_ip_address: record.creator_ip_address,
|
||||
creator_set_visibility: record.creator_set_visibility,
|
||||
maybe_prompt_token: record.maybe_prompt_token,
|
||||
maybe_wallet_ledger_entry_token: record.maybe_wallet_ledger_entry_token,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(jobs)
|
||||
}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
pub mod insert_generic_inference_job_for_grok_api;
|
||||
pub mod list_pending_grok_api_jobs;
|
||||
+1
@@ -2,5 +2,6 @@ pub mod beeble;
|
||||
pub(crate) mod common;
|
||||
pub mod fal;
|
||||
pub mod gmicloud;
|
||||
pub mod grok_api;
|
||||
pub mod seedance2pro;
|
||||
pub mod worldlabs;
|
||||
|
||||
@@ -21,6 +21,7 @@ pub enum DebugLogType {
|
||||
HttpRequest,
|
||||
FalRequest,
|
||||
KinoviRequest,
|
||||
GrokApiRequest,
|
||||
FalWebhook,
|
||||
BeebleWebhook,
|
||||
}
|
||||
@@ -35,6 +36,7 @@ impl DebugLogType {
|
||||
Self::HttpRequest => "http_request",
|
||||
Self::FalRequest => "fal_request",
|
||||
Self::KinoviRequest => "kinovi_request",
|
||||
Self::GrokApiRequest => "grok_api_request",
|
||||
Self::FalWebhook => "fal_webhook",
|
||||
Self::BeebleWebhook => "beeble_webhook",
|
||||
}
|
||||
@@ -45,6 +47,7 @@ impl DebugLogType {
|
||||
"http_request" => Ok(Self::HttpRequest),
|
||||
"fal_request" => Ok(Self::FalRequest),
|
||||
"kinovi_request" => Ok(Self::KinoviRequest),
|
||||
"grok_api_request" => Ok(Self::GrokApiRequest),
|
||||
"fal_webhook" => Ok(Self::FalWebhook),
|
||||
"beeble_webhook" => Ok(Self::BeebleWebhook),
|
||||
_ => Err(EnumError::CouldNotConvertFromString(value.to_string())),
|
||||
@@ -56,6 +59,7 @@ impl DebugLogType {
|
||||
Self::HttpRequest,
|
||||
Self::FalRequest,
|
||||
Self::KinoviRequest,
|
||||
Self::GrokApiRequest,
|
||||
Self::FalWebhook,
|
||||
Self::BeebleWebhook,
|
||||
])
|
||||
@@ -77,6 +81,7 @@ mod tests {
|
||||
assert_serialization(DebugLogType::HttpRequest, "http_request");
|
||||
assert_serialization(DebugLogType::FalRequest, "fal_request");
|
||||
assert_serialization(DebugLogType::KinoviRequest, "kinovi_request");
|
||||
assert_serialization(DebugLogType::GrokApiRequest, "grok_api_request");
|
||||
assert_serialization(DebugLogType::FalWebhook, "fal_webhook");
|
||||
assert_serialization(DebugLogType::BeebleWebhook, "beeble_webhook");
|
||||
}
|
||||
@@ -86,6 +91,7 @@ mod tests {
|
||||
assert_eq!(DebugLogType::HttpRequest.to_str(), "http_request");
|
||||
assert_eq!(DebugLogType::FalRequest.to_str(), "fal_request");
|
||||
assert_eq!(DebugLogType::KinoviRequest.to_str(), "kinovi_request");
|
||||
assert_eq!(DebugLogType::GrokApiRequest.to_str(), "grok_api_request");
|
||||
assert_eq!(DebugLogType::FalWebhook.to_str(), "fal_webhook");
|
||||
assert_eq!(DebugLogType::BeebleWebhook.to_str(), "beeble_webhook");
|
||||
}
|
||||
@@ -95,6 +101,7 @@ mod tests {
|
||||
assert_eq!(DebugLogType::from_str("http_request").unwrap(), DebugLogType::HttpRequest);
|
||||
assert_eq!(DebugLogType::from_str("fal_request").unwrap(), DebugLogType::FalRequest);
|
||||
assert_eq!(DebugLogType::from_str("kinovi_request").unwrap(), DebugLogType::KinoviRequest);
|
||||
assert_eq!(DebugLogType::from_str("grok_api_request").unwrap(), DebugLogType::GrokApiRequest);
|
||||
assert_eq!(DebugLogType::from_str("fal_webhook").unwrap(), DebugLogType::FalWebhook);
|
||||
assert_eq!(DebugLogType::from_str("beeble_webhook").unwrap(), DebugLogType::BeebleWebhook);
|
||||
}
|
||||
@@ -113,10 +120,11 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = DebugLogType::all_variants();
|
||||
assert_eq!(variants.len(), 5);
|
||||
assert_eq!(variants.len(), 6);
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::HttpRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::FalRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::KinoviRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::GrokApiRequest));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::FalWebhook));
|
||||
assert_eq!(variants.pop_first(), Some(DebugLogType::BeebleWebhook));
|
||||
assert_eq!(variants.pop_first(), None);
|
||||
|
||||
+11
-1
@@ -25,6 +25,10 @@ pub enum InferenceJobExternalThirdParty {
|
||||
#[serde(rename = "gmicloud")]
|
||||
GmiCloud,
|
||||
|
||||
/// Grok (xAI) API jobs
|
||||
#[serde(rename = "grok_api")]
|
||||
GrokApi,
|
||||
|
||||
/// Seedance 2 Pro jobs
|
||||
#[serde(rename = "seedance2pro")]
|
||||
Seedance2Pro,
|
||||
@@ -49,6 +53,7 @@ impl InferenceJobExternalThirdParty {
|
||||
Self::Beeble => "beeble",
|
||||
Self::Fal => "fal",
|
||||
Self::GmiCloud => "gmicloud",
|
||||
Self::GrokApi => "grok_api",
|
||||
Self::Seedance2Pro => "seedance2pro",
|
||||
Self::Seedance2ProAlt => "seedance2pro_alt",
|
||||
Self::Worldlabs => "worldlabs",
|
||||
@@ -60,6 +65,7 @@ impl InferenceJobExternalThirdParty {
|
||||
"beeble" => Ok(Self::Beeble),
|
||||
"fal" => Ok(Self::Fal),
|
||||
"gmicloud" => Ok(Self::GmiCloud),
|
||||
"grok_api" => Ok(Self::GrokApi),
|
||||
"seedance2pro" => Ok(Self::Seedance2Pro),
|
||||
"seedance2pro_alt" => Ok(Self::Seedance2ProAlt),
|
||||
"worldlabs" => Ok(Self::Worldlabs),
|
||||
@@ -74,6 +80,7 @@ impl InferenceJobExternalThirdParty {
|
||||
Self::Beeble,
|
||||
Self::Fal,
|
||||
Self::GmiCloud,
|
||||
Self::GrokApi,
|
||||
Self::Seedance2Pro,
|
||||
Self::Seedance2ProAlt,
|
||||
Self::Worldlabs,
|
||||
@@ -94,6 +101,7 @@ mod tests {
|
||||
assert_serialization(InferenceJobExternalThirdParty::Beeble, "beeble");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Fal, "fal");
|
||||
assert_serialization(InferenceJobExternalThirdParty::GmiCloud, "gmicloud");
|
||||
assert_serialization(InferenceJobExternalThirdParty::GrokApi, "grok_api");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Seedance2Pro, "seedance2pro");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Seedance2ProAlt, "seedance2pro_alt");
|
||||
assert_serialization(InferenceJobExternalThirdParty::Worldlabs, "worldlabs");
|
||||
@@ -104,6 +112,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobExternalThirdParty::Beeble.to_str(), "beeble");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Fal.to_str(), "fal");
|
||||
assert_eq!(InferenceJobExternalThirdParty::GmiCloud.to_str(), "gmicloud");
|
||||
assert_eq!(InferenceJobExternalThirdParty::GrokApi.to_str(), "grok_api");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Seedance2Pro.to_str(), "seedance2pro");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Seedance2ProAlt.to_str(), "seedance2pro_alt");
|
||||
assert_eq!(InferenceJobExternalThirdParty::Worldlabs.to_str(), "worldlabs");
|
||||
@@ -114,6 +123,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("beeble").unwrap(), InferenceJobExternalThirdParty::Beeble);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("fal").unwrap(), InferenceJobExternalThirdParty::Fal);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("gmicloud").unwrap(), InferenceJobExternalThirdParty::GmiCloud);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("grok_api").unwrap(), InferenceJobExternalThirdParty::GrokApi);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro").unwrap(), InferenceJobExternalThirdParty::Seedance2Pro);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("seedance2pro_alt").unwrap(), InferenceJobExternalThirdParty::Seedance2ProAlt);
|
||||
assert_eq!(InferenceJobExternalThirdParty::from_str("worldlabs").unwrap(), InferenceJobExternalThirdParty::Worldlabs);
|
||||
@@ -122,7 +132,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 6;
|
||||
const EXPECTED_COUNT : usize = 7;
|
||||
|
||||
assert_eq!(InferenceJobExternalThirdParty::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobExternalThirdParty::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+12
-1
@@ -35,6 +35,11 @@ pub enum InferenceJobProductCategory {
|
||||
#[serde(rename = "gmicloud_video")]
|
||||
GmiCloudVideo,
|
||||
|
||||
// =============== GROK (xAI) ===============
|
||||
|
||||
#[serde(rename = "grok_api_video")]
|
||||
GrokApiVideo,
|
||||
|
||||
// =============== BEEBLE ===============
|
||||
|
||||
#[serde(rename = "beeble_video")]
|
||||
@@ -155,6 +160,7 @@ impl InferenceJobProductCategory {
|
||||
Self::FalObject => "fal_object",
|
||||
Self::FalBgRemoval => "fal_bg_removal",
|
||||
Self::GmiCloudVideo => "gmicloud_video",
|
||||
Self::GrokApiVideo => "grok_api_video",
|
||||
Self::BeebleVideo => "beeble_video",
|
||||
Self::Seedance2ProVideo => "seedance2pro_video",
|
||||
Self::Seedance2ProCharacter => "seedance2pro_character",
|
||||
@@ -193,6 +199,7 @@ impl InferenceJobProductCategory {
|
||||
"fal_object" => Ok(Self::FalObject),
|
||||
"fal_bg_removal" => Ok(Self::FalBgRemoval),
|
||||
"gmicloud_video" => Ok(Self::GmiCloudVideo),
|
||||
"grok_api_video" => Ok(Self::GrokApiVideo),
|
||||
"beeble_video" => Ok(Self::BeebleVideo),
|
||||
"seedance2pro_video" => Ok(Self::Seedance2ProVideo),
|
||||
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
|
||||
@@ -234,6 +241,7 @@ impl InferenceJobProductCategory {
|
||||
Self::FalObject,
|
||||
Self::FalBgRemoval,
|
||||
Self::GmiCloudVideo,
|
||||
Self::GrokApiVideo,
|
||||
Self::BeebleVideo,
|
||||
Self::Seedance2ProVideo,
|
||||
Self::Seedance2ProCharacter,
|
||||
@@ -281,6 +289,7 @@ mod tests {
|
||||
assert_serialization(InferenceJobProductCategory::FalObject, "fal_object");
|
||||
assert_serialization(InferenceJobProductCategory::FalBgRemoval, "fal_bg_removal");
|
||||
assert_serialization(InferenceJobProductCategory::GmiCloudVideo, "gmicloud_video");
|
||||
assert_serialization(InferenceJobProductCategory::GrokApiVideo, "grok_api_video");
|
||||
assert_serialization(InferenceJobProductCategory::BeebleVideo, "beeble_video");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProVideo, "seedance2pro_video");
|
||||
assert_serialization(InferenceJobProductCategory::Seedance2ProCharacter, "seedance2pro_character");
|
||||
@@ -318,6 +327,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobProductCategory::FalObject.to_str(), "fal_object");
|
||||
assert_eq!(InferenceJobProductCategory::FalBgRemoval.to_str(), "fal_bg_removal");
|
||||
assert_eq!(InferenceJobProductCategory::GmiCloudVideo.to_str(), "gmicloud_video");
|
||||
assert_eq!(InferenceJobProductCategory::GrokApiVideo.to_str(), "grok_api_video");
|
||||
assert_eq!(InferenceJobProductCategory::BeebleVideo.to_str(), "beeble_video");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProVideo.to_str(), "seedance2pro_video");
|
||||
assert_eq!(InferenceJobProductCategory::Seedance2ProCharacter.to_str(), "seedance2pro_character");
|
||||
@@ -354,6 +364,7 @@ mod tests {
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_object").unwrap(), InferenceJobProductCategory::FalObject);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("fal_bg_removal").unwrap(), InferenceJobProductCategory::FalBgRemoval);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("gmicloud_video").unwrap(), InferenceJobProductCategory::GmiCloudVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("grok_api_video").unwrap(), InferenceJobProductCategory::GrokApiVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("beeble_video").unwrap(), InferenceJobProductCategory::BeebleVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_video").unwrap(), InferenceJobProductCategory::Seedance2ProVideo);
|
||||
assert_eq!(InferenceJobProductCategory::from_str("seedance2pro_character").unwrap(), InferenceJobProductCategory::Seedance2ProCharacter);
|
||||
@@ -385,7 +396,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 33;
|
||||
const EXPECTED_COUNT : usize = 34;
|
||||
|
||||
assert_eq!(InferenceJobProductCategory::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobProductCategory::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+11
-1
@@ -23,6 +23,10 @@ pub enum InferenceJobType {
|
||||
#[serde(rename = "gmicloud_queue")]
|
||||
GmiCloudQueue,
|
||||
|
||||
/// Grok (xAI) API jobs. We poll for results.
|
||||
#[serde(rename = "grok_api_queue")]
|
||||
GrokApiQueue,
|
||||
|
||||
/// Seedance 2 Pro jobs. We poll for results.
|
||||
#[serde(rename = "seedance2pro_queue")]
|
||||
Seedance2ProQueue,
|
||||
@@ -135,6 +139,7 @@ impl InferenceJobType {
|
||||
match self {
|
||||
Self::FalQueue => "fal_queue",
|
||||
Self::GmiCloudQueue => "gmicloud_queue",
|
||||
Self::GrokApiQueue => "grok_api_queue",
|
||||
Self::BeebleQueue => "beeble_queue",
|
||||
Self::Seedance2ProQueue => "seedance2pro_queue",
|
||||
Self::Seedance2ProAltQueue => "seedance2pro_alt_queue",
|
||||
@@ -167,6 +172,7 @@ impl InferenceJobType {
|
||||
match value {
|
||||
"fal_queue" => Ok(Self::FalQueue),
|
||||
"gmicloud_queue" => Ok(Self::GmiCloudQueue),
|
||||
"grok_api_queue" => Ok(Self::GrokApiQueue),
|
||||
"beeble_queue" => Ok(Self::BeebleQueue),
|
||||
"seedance2pro_queue" => Ok(Self::Seedance2ProQueue),
|
||||
"seedance2pro_character" => Ok(Self::Seedance2ProCharacter),
|
||||
@@ -202,6 +208,7 @@ impl InferenceJobType {
|
||||
BTreeSet::from([
|
||||
Self::FalQueue,
|
||||
Self::GmiCloudQueue,
|
||||
Self::GrokApiQueue,
|
||||
Self::BeebleQueue,
|
||||
Self::Seedance2ProQueue,
|
||||
Self::Seedance2ProCharacter,
|
||||
@@ -248,6 +255,7 @@ mod tests {
|
||||
fn test_serialization() {
|
||||
assert_serialization(InferenceJobType::FalQueue, "fal_queue");
|
||||
assert_serialization(InferenceJobType::GmiCloudQueue, "gmicloud_queue");
|
||||
assert_serialization(InferenceJobType::GrokApiQueue, "grok_api_queue");
|
||||
assert_serialization(InferenceJobType::BeebleQueue, "beeble_queue");
|
||||
assert_serialization(InferenceJobType::Seedance2ProQueue, "seedance2pro_queue");
|
||||
assert_serialization(InferenceJobType::Seedance2ProCharacter, "seedance2pro_character");
|
||||
@@ -279,6 +287,7 @@ mod tests {
|
||||
fn to_str() {
|
||||
assert_eq!(InferenceJobType::FalQueue.to_str(), "fal_queue");
|
||||
assert_eq!(InferenceJobType::GmiCloudQueue.to_str(), "gmicloud_queue");
|
||||
assert_eq!(InferenceJobType::GrokApiQueue.to_str(), "grok_api_queue");
|
||||
assert_eq!(InferenceJobType::BeebleQueue.to_str(), "beeble_queue");
|
||||
assert_eq!(InferenceJobType::Seedance2ProQueue.to_str(), "seedance2pro_queue");
|
||||
assert_eq!(InferenceJobType::Seedance2ProCharacter.to_str(), "seedance2pro_character");
|
||||
@@ -310,6 +319,7 @@ mod tests {
|
||||
fn from_str() {
|
||||
assert_eq!(InferenceJobType::from_str("fal_queue").unwrap(), InferenceJobType::FalQueue);
|
||||
assert_eq!(InferenceJobType::from_str("gmicloud_queue").unwrap(), InferenceJobType::GmiCloudQueue);
|
||||
assert_eq!(InferenceJobType::from_str("grok_api_queue").unwrap(), InferenceJobType::GrokApiQueue);
|
||||
assert_eq!(InferenceJobType::from_str("beeble_queue").unwrap(), InferenceJobType::BeebleQueue);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_queue").unwrap(), InferenceJobType::Seedance2ProQueue);
|
||||
assert_eq!(InferenceJobType::from_str("seedance2pro_character").unwrap(), InferenceJobType::Seedance2ProCharacter);
|
||||
@@ -340,7 +350,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
// Static check
|
||||
const EXPECTED_COUNT : usize = 27;
|
||||
const EXPECTED_COUNT : usize = 28;
|
||||
|
||||
assert_eq!(InferenceJobType::all_variants().len(), EXPECTED_COUNT);
|
||||
assert_eq!(InferenceJobType::iter().len(), EXPECTED_COUNT);
|
||||
|
||||
+27
@@ -101,6 +101,10 @@ pub enum InferenceModelType {
|
||||
GptImage2,
|
||||
#[serde(rename = "grok_image")]
|
||||
GrokImage,
|
||||
#[serde(rename = "grok_imagine_image")]
|
||||
GrokImagineImage,
|
||||
#[serde(rename = "grok_imagine_image_q")]
|
||||
GrokImagineImageQuality,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
#[serde(rename = "seededit_3")]
|
||||
@@ -143,6 +147,8 @@ pub enum InferenceModelType {
|
||||
// Video models
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
@@ -251,6 +257,8 @@ impl InferenceModelType {
|
||||
Self::GptImage1p5 => "gpt_image_1p5",
|
||||
Self::GptImage2 => "gpt_image_2",
|
||||
Self::GrokImage => "grok_image",
|
||||
Self::GrokImagineImage => "grok_imagine_image",
|
||||
Self::GrokImagineImageQuality => "grok_imagine_image_q",
|
||||
Self::Recraft3 => "recraft_3",
|
||||
Self::SeedEdit3 => "seededit_3",
|
||||
Self::Qwen => "qwen",
|
||||
@@ -273,6 +281,7 @@ impl InferenceModelType {
|
||||
|
||||
// Video models
|
||||
Self::GrokVideo => "grok_video",
|
||||
Self::GrokImagineVideo => "grok_imagine_video",
|
||||
Self::Kling16Pro => "kling_1p6_pro",
|
||||
Self::Kling21Pro => "kling_2p1_pro",
|
||||
Self::Kling21Master => "kling_2p1_master",
|
||||
@@ -344,6 +353,8 @@ impl InferenceModelType {
|
||||
"gpt_image_1p5" => Ok(Self::GptImage1p5),
|
||||
"gpt_image_2" => Ok(Self::GptImage2),
|
||||
"grok_image" => Ok(Self::GrokImage),
|
||||
"grok_imagine_image" => Ok(Self::GrokImagineImage),
|
||||
"grok_imagine_image_q" => Ok(Self::GrokImagineImageQuality),
|
||||
"recraft_3" => Ok(Self::Recraft3),
|
||||
"seededit_3" => Ok(Self::SeedEdit3),
|
||||
"qwen" => Ok(Self::Qwen),
|
||||
@@ -366,6 +377,7 @@ impl InferenceModelType {
|
||||
|
||||
// Video models
|
||||
"grok_video" => Ok(Self::GrokVideo),
|
||||
"grok_imagine_video" => Ok(Self::GrokImagineVideo),
|
||||
"kling_1p6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2p1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2p1_master" => Ok(Self::Kling21Master),
|
||||
@@ -441,6 +453,8 @@ impl InferenceModelType {
|
||||
Self::GptImage1p5,
|
||||
Self::GptImage2,
|
||||
Self::GrokImage,
|
||||
Self::GrokImagineImage,
|
||||
Self::GrokImagineImageQuality,
|
||||
Self::Recraft3,
|
||||
Self::SeedEdit3,
|
||||
Self::Qwen,
|
||||
@@ -463,6 +477,7 @@ impl InferenceModelType {
|
||||
|
||||
// Video models
|
||||
Self::GrokVideo,
|
||||
Self::GrokImagineVideo,
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
@@ -521,6 +536,8 @@ impl InferenceModelType {
|
||||
CommonModelType::GptImage1p5 => Self::GptImage1p5,
|
||||
CommonModelType::GptImage2 => Self::GptImage2,
|
||||
CommonModelType::GrokImage => Self::GrokImage,
|
||||
CommonModelType::GrokImagineImage => Self::GrokImagineImage,
|
||||
CommonModelType::GrokImagineImageQuality => Self::GrokImagineImageQuality,
|
||||
CommonModelType::Recraft3 => Self::Recraft3,
|
||||
CommonModelType::SeedEdit3 => Self::SeedEdit3,
|
||||
CommonModelType::Qwen => Self::Qwen,
|
||||
@@ -543,6 +560,7 @@ impl InferenceModelType {
|
||||
|
||||
// Video models
|
||||
CommonModelType::GrokVideo => Self::GrokVideo,
|
||||
CommonModelType::GrokImagineVideo => Self::GrokImagineVideo,
|
||||
CommonModelType::Kling16Pro => Self::Kling16Pro,
|
||||
CommonModelType::Kling21Pro => Self::Kling21Pro,
|
||||
CommonModelType::Kling21Master => Self::Kling21Master,
|
||||
@@ -624,6 +642,8 @@ mod tests {
|
||||
assert_serialization(InferenceModelType::GptImage1p5, "gpt_image_1p5");
|
||||
assert_serialization(InferenceModelType::GptImage2, "gpt_image_2");
|
||||
assert_serialization(InferenceModelType::GrokImage, "grok_image");
|
||||
assert_serialization(InferenceModelType::GrokImagineImage, "grok_imagine_image");
|
||||
assert_serialization(InferenceModelType::GrokImagineImageQuality, "grok_imagine_image_q");
|
||||
assert_serialization(InferenceModelType::Recraft3, "recraft_3");
|
||||
assert_serialization(InferenceModelType::SeedEdit3, "seededit_3");
|
||||
assert_serialization(InferenceModelType::Qwen, "qwen");
|
||||
@@ -646,6 +666,7 @@ mod tests {
|
||||
|
||||
// Video models
|
||||
assert_serialization(InferenceModelType::GrokVideo, "grok_video");
|
||||
assert_serialization(InferenceModelType::GrokImagineVideo, "grok_imagine_video");
|
||||
assert_serialization(InferenceModelType::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(InferenceModelType::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(InferenceModelType::Kling21Master, "kling_2p1_master");
|
||||
@@ -716,6 +737,8 @@ mod tests {
|
||||
assert_eq!(InferenceModelType::GptImage1p5.to_str(), "gpt_image_1p5");
|
||||
assert_eq!(InferenceModelType::GptImage2.to_str(), "gpt_image_2");
|
||||
assert_eq!(InferenceModelType::GrokImage.to_str(), "grok_image");
|
||||
assert_eq!(InferenceModelType::GrokImagineImage.to_str(), "grok_imagine_image");
|
||||
assert_eq!(InferenceModelType::GrokImagineImageQuality.to_str(), "grok_imagine_image_q");
|
||||
assert_eq!(InferenceModelType::Recraft3.to_str(), "recraft_3");
|
||||
assert_eq!(InferenceModelType::SeedEdit3.to_str(), "seededit_3");
|
||||
assert_eq!(InferenceModelType::Qwen.to_str(), "qwen");
|
||||
@@ -738,6 +761,7 @@ mod tests {
|
||||
|
||||
// Video models
|
||||
assert_eq!(InferenceModelType::GrokVideo.to_str(), "grok_video");
|
||||
assert_eq!(InferenceModelType::GrokImagineVideo.to_str(), "grok_imagine_video");
|
||||
assert_eq!(InferenceModelType::Kling16Pro.to_str(), "kling_1p6_pro");
|
||||
assert_eq!(InferenceModelType::Kling21Pro.to_str(), "kling_2p1_pro");
|
||||
assert_eq!(InferenceModelType::Kling21Master.to_str(), "kling_2p1_master");
|
||||
@@ -808,6 +832,8 @@ mod tests {
|
||||
assert_eq!(InferenceModelType::from_str("gpt_image_1p5").unwrap(), InferenceModelType::GptImage1p5);
|
||||
assert_eq!(InferenceModelType::from_str("gpt_image_2").unwrap(), InferenceModelType::GptImage2);
|
||||
assert_eq!(InferenceModelType::from_str("grok_image").unwrap(), InferenceModelType::GrokImage);
|
||||
assert_eq!(InferenceModelType::from_str("grok_imagine_image").unwrap(), InferenceModelType::GrokImagineImage);
|
||||
assert_eq!(InferenceModelType::from_str("grok_imagine_image_q").unwrap(), InferenceModelType::GrokImagineImageQuality);
|
||||
assert_eq!(InferenceModelType::from_str("recraft_3").unwrap(), InferenceModelType::Recraft3);
|
||||
assert_eq!(InferenceModelType::from_str("seededit_3").unwrap(), InferenceModelType::SeedEdit3);
|
||||
assert_eq!(InferenceModelType::from_str("qwen").unwrap(), InferenceModelType::Qwen);
|
||||
@@ -830,6 +856,7 @@ mod tests {
|
||||
|
||||
// Video models
|
||||
assert_eq!(InferenceModelType::from_str("grok_video").unwrap(), InferenceModelType::GrokVideo);
|
||||
assert_eq!(InferenceModelType::from_str("grok_imagine_video").unwrap(), InferenceModelType::GrokImagineVideo);
|
||||
assert_eq!(InferenceModelType::from_str("kling_1p6_pro").unwrap(), InferenceModelType::Kling16Pro);
|
||||
assert_eq!(InferenceModelType::from_str("kling_2p1_pro").unwrap(), InferenceModelType::Kling21Pro);
|
||||
assert_eq!(InferenceModelType::from_str("kling_2p1_master").unwrap(), InferenceModelType::Kling21Master);
|
||||
|
||||
@@ -25,6 +25,10 @@ pub enum CommonImageModel {
|
||||
GptImage1p5,
|
||||
#[serde(rename = "gpt_image_2")]
|
||||
GptImage2,
|
||||
#[serde(rename = "grok_imagine_image")]
|
||||
GrokImagineImage,
|
||||
#[serde(rename = "grok_imagine_image_q")]
|
||||
GrokImagineImageQuality,
|
||||
#[serde(rename = "nano_banana")]
|
||||
NanoBanana,
|
||||
#[serde(rename = "nano_banana_2")]
|
||||
@@ -50,6 +54,8 @@ impl CommonImageModel {
|
||||
Self::GptImage1 => CommonModelType::GptImage1,
|
||||
Self::GptImage1p5 => CommonModelType::GptImage1p5,
|
||||
Self::GptImage2 => CommonModelType::GptImage2,
|
||||
Self::GrokImagineImage => CommonModelType::GrokImagineImage,
|
||||
Self::GrokImagineImageQuality => CommonModelType::GrokImagineImageQuality,
|
||||
Self::NanoBanana => CommonModelType::NanoBanana,
|
||||
Self::NanoBanana2 => CommonModelType::NanoBanana2,
|
||||
Self::NanoBananaPro => CommonModelType::NanoBananaPro,
|
||||
@@ -79,6 +85,8 @@ mod tests {
|
||||
assert_serialization(CommonImageModel::GptImage1, "gpt_image_1");
|
||||
assert_serialization(CommonImageModel::GptImage1p5, "gpt_image_1p5");
|
||||
assert_serialization(CommonImageModel::GptImage2, "gpt_image_2");
|
||||
assert_serialization(CommonImageModel::GrokImagineImage, "grok_imagine_image");
|
||||
assert_serialization(CommonImageModel::GrokImagineImageQuality, "grok_imagine_image_q");
|
||||
assert_serialization(CommonImageModel::NanoBanana, "nano_banana");
|
||||
assert_serialization(CommonImageModel::NanoBanana2, "nano_banana_2");
|
||||
assert_serialization(CommonImageModel::NanoBananaPro, "nano_banana_pro");
|
||||
@@ -97,6 +105,8 @@ mod tests {
|
||||
(CommonImageModel::GptImage1, CommonModelType::GptImage1),
|
||||
(CommonImageModel::GptImage1p5, CommonModelType::GptImage1p5),
|
||||
(CommonImageModel::GptImage2, CommonModelType::GptImage2),
|
||||
(CommonImageModel::GrokImagineImage, CommonModelType::GrokImagineImage),
|
||||
(CommonImageModel::GrokImagineImageQuality, CommonModelType::GrokImagineImageQuality),
|
||||
(CommonImageModel::NanoBanana, CommonModelType::NanoBanana),
|
||||
(CommonImageModel::NanoBanana2, CommonModelType::NanoBanana2),
|
||||
(CommonImageModel::NanoBananaPro, CommonModelType::NanoBananaPro),
|
||||
@@ -129,7 +139,7 @@ mod tests {
|
||||
#[test]
|
||||
fn variant_count() {
|
||||
use strum::IntoEnumIterator;
|
||||
assert_eq!(CommonImageModel::iter().len(), 13);
|
||||
assert_eq!(CommonImageModel::iter().len(), 15);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -53,6 +53,12 @@ pub enum CommonModelType {
|
||||
// Generic grok image model without a version
|
||||
#[serde(rename = "grok_image")]
|
||||
GrokImage,
|
||||
// xAI's Grok Imagine image model (grok-imagine-image), standard tier.
|
||||
#[serde(rename = "grok_imagine_image")]
|
||||
GrokImagineImage,
|
||||
// xAI's Grok Imagine image model (grok-imagine-image-quality), high-quality tier.
|
||||
#[serde(rename = "grok_imagine_image_q")]
|
||||
GrokImagineImageQuality,
|
||||
#[serde(rename = "recraft_3")]
|
||||
Recraft3,
|
||||
#[serde(rename = "seededit_3")]
|
||||
@@ -101,9 +107,16 @@ pub enum CommonModelType {
|
||||
|
||||
// Video models
|
||||
|
||||
// Generic grok video model without a version
|
||||
/// Generic grok video model without a version
|
||||
/// NB: This was for the consumer web login API
|
||||
#[deprecated(note = "use GrokImagineVideo instead")]
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
GrokVideo,
|
||||
/// xAI's Grok Imagine video model (grok-imagine-video).
|
||||
/// This is for the API-based version.
|
||||
/// No version is specified.
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
#[serde(rename = "kling_2p1_pro")]
|
||||
@@ -196,6 +209,8 @@ impl CommonModelType {
|
||||
Self::GptImage1p5 => "gpt_image_1p5",
|
||||
Self::GptImage2 => "gpt_image_2",
|
||||
Self::GrokImage => "grok_image",
|
||||
Self::GrokImagineImage => "grok_imagine_image",
|
||||
Self::GrokImagineImageQuality => "grok_imagine_image_q",
|
||||
Self::Recraft3 => "recraft_3",
|
||||
Self::SeedEdit3 => "seededit_3",
|
||||
Self::Qwen => "qwen",
|
||||
@@ -218,6 +233,7 @@ impl CommonModelType {
|
||||
|
||||
// Video models
|
||||
Self::GrokVideo => "grok_video",
|
||||
Self::GrokImagineVideo => "grok_imagine_video",
|
||||
Self::Kling16Pro => "kling_1p6_pro",
|
||||
Self::Kling21Pro => "kling_2p1_pro",
|
||||
Self::Kling21Master => "kling_2p1_master",
|
||||
@@ -272,6 +288,8 @@ impl CommonModelType {
|
||||
"gpt_image_1p5" => Ok(Self::GptImage1p5),
|
||||
"gpt_image_2" => Ok(Self::GptImage2),
|
||||
"grok_image" => Ok(Self::GrokImage),
|
||||
"grok_imagine_image" => Ok(Self::GrokImagineImage),
|
||||
"grok_imagine_image_q" => Ok(Self::GrokImagineImageQuality),
|
||||
"recraft_3" => Ok(Self::Recraft3),
|
||||
"seededit_3" => Ok(Self::SeedEdit3),
|
||||
"qwen" => Ok(Self::Qwen),
|
||||
@@ -294,6 +312,7 @@ impl CommonModelType {
|
||||
|
||||
// Video models
|
||||
"grok_video" => Ok(Self::GrokVideo),
|
||||
"grok_imagine_video" => Ok(Self::GrokImagineVideo),
|
||||
"kling_1p6_pro" => Ok(Self::Kling16Pro),
|
||||
"kling_2p1_pro" => Ok(Self::Kling21Pro),
|
||||
"kling_2p1_master" => Ok(Self::Kling21Master),
|
||||
@@ -352,6 +371,8 @@ impl CommonModelType {
|
||||
Self::GptImage1p5,
|
||||
Self::GptImage2,
|
||||
Self::GrokImage,
|
||||
Self::GrokImagineImage,
|
||||
Self::GrokImagineImageQuality,
|
||||
Self::Recraft3,
|
||||
Self::SeedEdit3,
|
||||
Self::Qwen,
|
||||
@@ -374,6 +395,7 @@ impl CommonModelType {
|
||||
|
||||
// Video models
|
||||
Self::GrokVideo,
|
||||
Self::GrokImagineVideo,
|
||||
Self::Kling16Pro,
|
||||
Self::Kling21Pro,
|
||||
Self::Kling21Master,
|
||||
@@ -429,6 +451,8 @@ impl CommonModelType {
|
||||
Self::GptImage1p5 => CommonModelClass::Image,
|
||||
Self::GptImage2 => CommonModelClass::Image,
|
||||
Self::GrokImage => CommonModelClass::Image,
|
||||
Self::GrokImagineImage => CommonModelClass::Image,
|
||||
Self::GrokImagineImageQuality => CommonModelClass::Image,
|
||||
Self::Recraft3 => CommonModelClass::Image,
|
||||
Self::SeedEdit3 => CommonModelClass::Image,
|
||||
Self::Qwen => CommonModelClass::Image,
|
||||
@@ -451,6 +475,7 @@ impl CommonModelType {
|
||||
|
||||
// Video models
|
||||
Self::GrokVideo => CommonModelClass::Video,
|
||||
Self::GrokImagineVideo => CommonModelClass::Video,
|
||||
Self::Kling16Pro => CommonModelClass::Video,
|
||||
Self::Kling21Pro => CommonModelClass::Video,
|
||||
Self::Kling21Master => CommonModelClass::Video,
|
||||
@@ -516,6 +541,8 @@ mod tests {
|
||||
assert_serialization(CommonModelType::GptImage1p5, "gpt_image_1p5");
|
||||
assert_serialization(CommonModelType::GptImage2, "gpt_image_2");
|
||||
assert_serialization(CommonModelType::GrokImage, "grok_image");
|
||||
assert_serialization(CommonModelType::GrokImagineImage, "grok_imagine_image");
|
||||
assert_serialization(CommonModelType::GrokImagineImageQuality, "grok_imagine_image_q");
|
||||
assert_serialization(CommonModelType::Recraft3, "recraft_3");
|
||||
assert_serialization(CommonModelType::SeedEdit3, "seededit_3");
|
||||
assert_serialization(CommonModelType::Qwen, "qwen");
|
||||
@@ -537,6 +564,7 @@ mod tests {
|
||||
assert_serialization(CommonModelType::MidjourneyV7Raw, "midjourney_v7_raw");
|
||||
// Video models
|
||||
assert_serialization(CommonModelType::GrokVideo, "grok_video");
|
||||
assert_serialization(CommonModelType::GrokImagineVideo, "grok_imagine_video");
|
||||
assert_serialization(CommonModelType::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(CommonModelType::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(CommonModelType::Kling21Master, "kling_2p1_master");
|
||||
@@ -588,6 +616,8 @@ mod tests {
|
||||
assert_eq!(CommonModelType::GptImage1p5.to_str(), "gpt_image_1p5");
|
||||
assert_eq!(CommonModelType::GptImage2.to_str(), "gpt_image_2");
|
||||
assert_eq!(CommonModelType::GrokImage.to_str(), "grok_image");
|
||||
assert_eq!(CommonModelType::GrokImagineImage.to_str(), "grok_imagine_image");
|
||||
assert_eq!(CommonModelType::GrokImagineImageQuality.to_str(), "grok_imagine_image_q");
|
||||
assert_eq!(CommonModelType::Recraft3.to_str(), "recraft_3");
|
||||
assert_eq!(CommonModelType::SeedEdit3.to_str(), "seededit_3");
|
||||
assert_eq!(CommonModelType::Qwen.to_str(), "qwen");
|
||||
@@ -610,6 +640,7 @@ mod tests {
|
||||
|
||||
// Video models
|
||||
assert_eq!(CommonModelType::GrokVideo.to_str(), "grok_video");
|
||||
assert_eq!(CommonModelType::GrokImagineVideo.to_str(), "grok_imagine_video");
|
||||
assert_eq!(CommonModelType::Kling16Pro.to_str(), "kling_1p6_pro");
|
||||
assert_eq!(CommonModelType::Kling21Pro.to_str(), "kling_2p1_pro");
|
||||
assert_eq!(CommonModelType::Kling21Master.to_str(), "kling_2p1_master");
|
||||
@@ -662,6 +693,8 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("gpt_image_1p5").unwrap(), CommonModelType::GptImage1p5);
|
||||
assert_eq!(CommonModelType::from_str("gpt_image_2").unwrap(), CommonModelType::GptImage2);
|
||||
assert_eq!(CommonModelType::from_str("grok_image").unwrap(), CommonModelType::GrokImage);
|
||||
assert_eq!(CommonModelType::from_str("grok_imagine_image").unwrap(), CommonModelType::GrokImagineImage);
|
||||
assert_eq!(CommonModelType::from_str("grok_imagine_image_q").unwrap(), CommonModelType::GrokImagineImageQuality);
|
||||
assert_eq!(CommonModelType::from_str("recraft_3").unwrap(), CommonModelType::Recraft3);
|
||||
assert_eq!(CommonModelType::from_str("seededit_3").unwrap(), CommonModelType::SeedEdit3);
|
||||
assert_eq!(CommonModelType::from_str("qwen").unwrap(), CommonModelType::Qwen);
|
||||
@@ -683,6 +716,7 @@ mod tests {
|
||||
assert_eq!(CommonModelType::from_str("midjourney_v7_raw").unwrap(), CommonModelType::MidjourneyV7Raw);
|
||||
// Video models
|
||||
assert_eq!(CommonModelType::from_str("grok_video").unwrap(), CommonModelType::GrokVideo);
|
||||
assert_eq!(CommonModelType::from_str("grok_imagine_video").unwrap(), CommonModelType::GrokImagineVideo);
|
||||
assert_eq!(CommonModelType::from_str("kling_1p6_pro").unwrap(), CommonModelType::Kling16Pro);
|
||||
assert_eq!(CommonModelType::from_str("kling_2p1_pro").unwrap(), CommonModelType::Kling21Pro);
|
||||
assert_eq!(CommonModelType::from_str("kling_2p1_master").unwrap(), CommonModelType::Kling21Master);
|
||||
@@ -723,7 +757,7 @@ mod tests {
|
||||
#[test]
|
||||
fn all_variants() {
|
||||
let mut variants = CommonModelType::all_variants();
|
||||
assert_eq!(variants.len(), 64);
|
||||
assert_eq!(variants.len(), 67);
|
||||
// Image models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Dev));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Flux1Schnell));
|
||||
@@ -737,6 +771,8 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GptImage1p5));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GptImage2));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokImage));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokImagineImage));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokImagineImageQuality));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Recraft3));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::SeedEdit3));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Qwen));
|
||||
@@ -758,6 +794,7 @@ mod tests {
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::MidjourneyV7Raw));
|
||||
// Video models
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokVideo));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::GrokImagineVideo));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling16Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling21Pro));
|
||||
assert_eq!(variants.pop_first(), Some(CommonModelType::Kling21Master));
|
||||
|
||||
@@ -5,9 +5,15 @@ use utoipa::ToSchema;
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CommonVideoModel {
|
||||
/// NB: This was for the web login version
|
||||
#[deprecated = "Use `GrokImagineVideo` instead."]
|
||||
#[serde(rename = "grok_video")]
|
||||
GrokVideo,
|
||||
|
||||
/// NB: This is for the API version
|
||||
#[serde(rename = "grok_imagine_video")]
|
||||
GrokImagineVideo,
|
||||
|
||||
#[serde(rename = "kling_1p6_pro")]
|
||||
Kling16Pro,
|
||||
|
||||
@@ -93,6 +99,7 @@ impl CommonVideoModel {
|
||||
use crate::common::generation::common_model_type::CommonModelType;
|
||||
match self {
|
||||
Self::GrokVideo => CommonModelType::GrokVideo,
|
||||
Self::GrokImagineVideo => CommonModelType::GrokImagineVideo,
|
||||
Self::Kling16Pro => CommonModelType::Kling16Pro,
|
||||
Self::Kling21Pro => CommonModelType::Kling21Pro,
|
||||
Self::Kling21Master => CommonModelType::Kling21Master,
|
||||
@@ -131,6 +138,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_serialization() {
|
||||
assert_serialization(CommonVideoModel::GrokVideo, "grok_video");
|
||||
assert_serialization(CommonVideoModel::GrokImagineVideo, "grok_imagine_video");
|
||||
assert_serialization(CommonVideoModel::Kling16Pro, "kling_1p6_pro");
|
||||
assert_serialization(CommonVideoModel::Kling21Pro, "kling_2p1_pro");
|
||||
assert_serialization(CommonVideoModel::Kling21Master, "kling_2p1_master");
|
||||
@@ -162,6 +170,7 @@ mod tests {
|
||||
fn test_deserialization() {
|
||||
let cases = [
|
||||
("grok_video", CommonVideoModel::GrokVideo),
|
||||
("grok_imagine_video", CommonVideoModel::GrokImagineVideo),
|
||||
("kling_1p6_pro", CommonVideoModel::Kling16Pro),
|
||||
("kling_2p1_pro", CommonVideoModel::Kling21Pro),
|
||||
("kling_2p1_master", CommonVideoModel::Kling21Master),
|
||||
@@ -200,6 +209,7 @@ mod tests {
|
||||
fn test_round_trip() {
|
||||
let all = [
|
||||
CommonVideoModel::GrokVideo,
|
||||
CommonVideoModel::GrokImagineVideo,
|
||||
CommonVideoModel::Kling16Pro,
|
||||
CommonVideoModel::Kling21Pro,
|
||||
CommonVideoModel::Kling21Master,
|
||||
@@ -237,6 +247,7 @@ mod tests {
|
||||
fn all_video_models_convert_to_common_model_type() {
|
||||
let models = [
|
||||
(CommonVideoModel::GrokVideo, CommonModelType::GrokVideo),
|
||||
(CommonVideoModel::GrokImagineVideo, CommonModelType::GrokImagineVideo),
|
||||
(CommonVideoModel::Kling16Pro, CommonModelType::Kling16Pro),
|
||||
(CommonVideoModel::Kling21Pro, CommonModelType::Kling21Pro),
|
||||
(CommonVideoModel::Kling21Master, CommonModelType::Kling21Master),
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "grok-api-job"
|
||||
edition = "2021"
|
||||
version = "0.0.1"
|
||||
publish = false
|
||||
|
||||
[[bin]]
|
||||
name = "grok-api-job"
|
||||
path = "src/main.rs"
|
||||
|
||||
[package.metadata.cargo-udeps.ignore]
|
||||
normal = ["workspace-build-acceleration"]
|
||||
|
||||
[dependencies]
|
||||
# Workspace hack (faster compile times)
|
||||
workspace-build-acceleration = { workspace = true }
|
||||
|
||||
# Internal
|
||||
actix_helpers.workspace = true
|
||||
bootstrap.workspace = true
|
||||
bucket_paths.workspace = true
|
||||
cloud_storage.workspace = true
|
||||
concurrency.workspace = true
|
||||
easyenv.workspace = true
|
||||
enums.workspace = true
|
||||
errors.workspace = true
|
||||
grok_api_client.workspace = true
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
jobs_common.workspace = true
|
||||
mysql_queries.workspace = true
|
||||
pager = { path = "../../../lib/pager" }
|
||||
rootly_client.workspace = true
|
||||
rootly_config.workspace = true
|
||||
server_environment = { path = "../../../lib/server_environment" }
|
||||
shared_env_var_config.workspace = true
|
||||
tokens.workspace = true
|
||||
|
||||
# External
|
||||
anyhow.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
sqlx = { version = "0.7.4", features = [ "mysql", "runtime-tokio-rustls", "chrono" ] }
|
||||
|
||||
# Runtime
|
||||
tokio = { version = "1.40.0", features = ["macros", "sync"] }
|
||||
|
||||
# HTTP Client
|
||||
reqwest.workspace = true
|
||||
|
||||
# HTTP Server
|
||||
actix-web.workspace = true
|
||||
|
||||
# Serialization
|
||||
serde.workspace = true
|
||||
serde_derive.workspace = true
|
||||
serde_json.workspace = true
|
||||
@@ -0,0 +1 @@
|
||||
SERVER_ENVIRONMENT='development'
|
||||
@@ -0,0 +1,101 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::error::ResponseError;
|
||||
use actix_web::http::header::ContentType;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use log::error;
|
||||
|
||||
use actix_helpers::response_serializers::error_to_json_http_response::error_to_json_http_response;
|
||||
|
||||
use crate::http_server::http_server_shared_state::HttpServerSharedState;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct HealthCheckResponse {
|
||||
pub success: bool,
|
||||
pub is_healthy: bool,
|
||||
pub consecutive_failure_count: u64,
|
||||
pub consecutive_success_count: u64,
|
||||
pub total_failure_count: u64,
|
||||
pub total_success_count: u64,
|
||||
pub total_failure_ratio: f32,
|
||||
pub total_success_ratio: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum HealthCheckError {
|
||||
ServerError,
|
||||
}
|
||||
|
||||
impl ResponseError for HealthCheckError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match *self {
|
||||
HealthCheckError::ServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
error_to_json_http_response(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for HealthCheckError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_health_check_handler(
|
||||
_http_request: HttpRequest,
|
||||
server_state: web::Data<Arc<HttpServerSharedState>>,
|
||||
) -> Result<HttpResponse, HealthCheckError> {
|
||||
let job_stats = server_state
|
||||
.job_stats
|
||||
.get_status()
|
||||
.map_err(|e| {
|
||||
error!("Error serving health check status: {:?}", e);
|
||||
HealthCheckError::ServerError
|
||||
})?;
|
||||
|
||||
let total_tries = job_stats
|
||||
.total_failure_count
|
||||
.saturating_add(job_stats.total_success_count);
|
||||
|
||||
let total_success_ratio = if total_tries > 0 {
|
||||
(job_stats.total_success_count as f32) / (total_tries as f32)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let total_failure_ratio = if total_tries > 0 {
|
||||
1.0 - total_success_ratio
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
let is_healthy =
|
||||
job_stats.consecutive_failure_count < server_state.consecutive_failure_unhealthy_threshold;
|
||||
|
||||
let response = HealthCheckResponse {
|
||||
success: true,
|
||||
is_healthy,
|
||||
consecutive_failure_count: job_stats.consecutive_failure_count,
|
||||
consecutive_success_count: job_stats.consecutive_success_count,
|
||||
total_failure_count: job_stats.total_failure_count,
|
||||
total_success_count: job_stats.total_success_count,
|
||||
total_failure_ratio,
|
||||
total_success_ratio,
|
||||
};
|
||||
|
||||
let body = serde_json::to_string(&response).map_err(|_e| HealthCheckError::ServerError)?;
|
||||
|
||||
if is_healthy {
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(ContentType::json())
|
||||
.body(body))
|
||||
} else {
|
||||
Ok(HttpResponse::InternalServerError()
|
||||
.content_type(ContentType::json())
|
||||
.body(body))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod health_check_handler;
|
||||
@@ -0,0 +1,7 @@
|
||||
use jobs_common::job_stats::JobStats;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct HttpServerSharedState {
|
||||
pub job_stats: JobStats,
|
||||
pub consecutive_failure_unhealthy_threshold: u64,
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod endpoints;
|
||||
pub mod http_server_shared_state;
|
||||
pub mod run_http_server;
|
||||
@@ -0,0 +1,68 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::dev::Server;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{web, App, HttpResponse, HttpServer};
|
||||
use log::info;
|
||||
|
||||
use bootstrap::bootstrap::ContainerEnvironment;
|
||||
use errors::AnyhowResult;
|
||||
use jobs_common::job_stats::JobStats;
|
||||
|
||||
use crate::http_server::endpoints::health_check_handler::get_health_check_handler;
|
||||
use crate::http_server::http_server_shared_state::HttpServerSharedState;
|
||||
|
||||
const DEFAULT_BIND_ADDRESS: &str = "0.0.0.0:12345";
|
||||
const DEFAULT_NUM_WORKERS: usize = 2;
|
||||
|
||||
pub struct CreateServerArgs {
|
||||
pub container_environment: ContainerEnvironment,
|
||||
pub job_stats: JobStats,
|
||||
}
|
||||
|
||||
pub fn run_http_server(args: CreateServerArgs) -> AnyhowResult<Server> {
|
||||
let bind_address = easyenv::get_env_string_or_default("HTTP_BIND_ADDRESS", DEFAULT_BIND_ADDRESS);
|
||||
let num_workers = easyenv::get_env_num("HTTP_NUM_WORKERS", DEFAULT_NUM_WORKERS)?;
|
||||
|
||||
let server_state = HttpServerSharedState {
|
||||
job_stats: args.job_stats.clone(),
|
||||
consecutive_failure_unhealthy_threshold: easyenv::get_env_num(
|
||||
"CONSECUTIVE_FAILURE_UNHEALTHY_THRESHOLD",
|
||||
3,
|
||||
)?,
|
||||
};
|
||||
|
||||
let _ = args.container_environment;
|
||||
|
||||
let server_state_arc = web::Data::new(Arc::new(server_state));
|
||||
|
||||
info!("Starting HTTP service (for k8s health checking).");
|
||||
|
||||
let log_format = "[%{HOSTNAME}e] IP=[%{X-Forwarded-For}i] \"%r\" %s %b \"%{Referer}i\" \"%{User-Agent}i\" %T";
|
||||
|
||||
let handle = HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(server_state_arc.clone())
|
||||
.wrap(Logger::new(&log_format).exclude("/_status"))
|
||||
.service(
|
||||
web::resource("/")
|
||||
.route(web::get().to(|| HttpResponse::Ok()))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
.service(
|
||||
web::resource("/_status")
|
||||
.route(web::get().to(get_health_check_handler))
|
||||
.route(web::head().to(|| HttpResponse::Ok())),
|
||||
)
|
||||
})
|
||||
.bind(bind_address)?
|
||||
.workers(num_workers)
|
||||
.run();
|
||||
|
||||
Ok(handle)
|
||||
}
|
||||
|
||||
pub async fn launch_http_server(args: CreateServerArgs) -> AnyhowResult<()> {
|
||||
run_http_server(args)?.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use concurrency::relaxed_atomic_bool::RelaxedAtomicBool;
|
||||
use grok_api_client::creds::grok_api_key::GrokApiKey;
|
||||
use jobs_common::job_stats::JobStats;
|
||||
use pager::client::pager::Pager;
|
||||
use server_environment::ServerEnvironment;
|
||||
use sqlx::MySqlPool;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct JobDependencies {
|
||||
pub mysql_pool: MySqlPool,
|
||||
|
||||
/// Public GCS/S3 bucket for storing generated videos.
|
||||
pub public_bucket_client: BucketClient,
|
||||
|
||||
/// API key for xAI's Imagine API.
|
||||
pub grok_api_key: GrokApiKey,
|
||||
|
||||
pub server_environment: ServerEnvironment,
|
||||
|
||||
pub pager: Pager,
|
||||
|
||||
pub job_stats: JobStats,
|
||||
|
||||
/// How long to sleep after a successful poll iteration (milliseconds).
|
||||
pub poll_interval_success_millis: u64,
|
||||
|
||||
/// How long to sleep after a failed poll iteration (milliseconds).
|
||||
pub poll_interval_failure_millis: u64,
|
||||
|
||||
/// How long to sleep between image_generation_job iterations (milliseconds).
|
||||
/// Stub-job for now — will be exercised once image generation is wired up.
|
||||
pub image_generation_poll_interval_millis: u64,
|
||||
|
||||
/// Set to `true` from another thread to trigger graceful shutdown.
|
||||
pub application_shutdown: RelaxedAtomicBool,
|
||||
|
||||
/// Notified when `application_shutdown` is set. Allows sleeping tasks
|
||||
/// to wake up immediately instead of waiting for the full sleep duration.
|
||||
pub shutdown_notify: Arc<Notify>,
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{info, warn};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
/// Stub loop for synchronous Grok image generation.
|
||||
///
|
||||
/// xAI's image endpoints (`/v1/images/generations`, `/v1/images/edits`) are
|
||||
/// synchronous, so this loop will eventually pull queued requests off our side
|
||||
/// instead of polling xAI for status. For now it ticks at the configured
|
||||
/// interval and does nothing — the next iteration of this work will fill it in.
|
||||
pub async fn image_generation_main_loop(job_dependencies: JobDependencies) {
|
||||
info!(
|
||||
"Grok image_generation_main_loop started (tick interval: {}ms). Stub — no work yet.",
|
||||
job_dependencies.image_generation_poll_interval_millis,
|
||||
);
|
||||
|
||||
while !job_dependencies.application_shutdown.get() {
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(job_dependencies.image_generation_poll_interval_millis)) => {}
|
||||
_ = job_dependencies.shutdown_notify.notified() => {}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("Grok image generation main loop is shut down.");
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod image_generation_main_loop;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod image_generation_job;
|
||||
pub mod video_polling_job;
|
||||
@@ -0,0 +1,34 @@
|
||||
use log::error;
|
||||
use mysql_queries::queries::generic_inference::api_providers::grok_api::list_pending_grok_api_jobs::PendingGrokApiJob;
|
||||
use pager::client::pager::Pager;
|
||||
use pager::notification::notification_details_builder::NotificationDetailsBuilder;
|
||||
use pager::notification::notification_urgency::NotificationUrgency;
|
||||
|
||||
/// Enqueue a pager alert for the error, then return it as `Err`.
|
||||
pub fn alert_pager_and_return_err<T>(
|
||||
pager: &Pager,
|
||||
title: &str,
|
||||
error: Box<dyn std::error::Error + Send + Sync + 'static>,
|
||||
job: Option<&PendingGrokApiJob>,
|
||||
) -> anyhow::Result<T> {
|
||||
let err_message = format!("{:#}", error);
|
||||
|
||||
let mut builder = NotificationDetailsBuilder::from_boxed_error(error)
|
||||
.set_title(title.to_string())
|
||||
.set_urgency(Some(NotificationUrgency::Medium));
|
||||
|
||||
if let Some(job) = job {
|
||||
builder = builder
|
||||
.set_inference_job_token(Some(job.job_token.to_string()))
|
||||
.set_third_party_id(Some(job.request_id.clone()))
|
||||
.set_user_token(job.maybe_creator_user_token.as_ref().map(|t| t.to_string()));
|
||||
}
|
||||
|
||||
let notification = builder.build();
|
||||
|
||||
if let Err(pager_err) = pager.enqueue_page(notification) {
|
||||
error!("Failed to enqueue pager alert: {:?}", pager_err);
|
||||
}
|
||||
|
||||
Err(anyhow::anyhow!(err_message))
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod alert_on_error;
|
||||
pub mod process_job;
|
||||
pub mod video_polling_main_loop;
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod process_failed_job;
|
||||
pub mod process_successful_job;
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
use log::{error, warn};
|
||||
|
||||
use enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory;
|
||||
use mysql_queries::queries::generic_inference::api_providers::grok_api::list_pending_grok_api_jobs::PendingGrokApiJob;
|
||||
use mysql_queries::queries::generic_inference::job::mark_job_failed_by_token::{mark_job_failed_by_token, MarkJobFailedByTokenArgs};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
|
||||
/// Mark the job failed in the database.
|
||||
///
|
||||
/// Unlike Seedance2Pro/Kinovi, Grok API video jobs are **non-refundable** —
|
||||
/// xAI's video generation is billed regardless of outcome, so we never issue
|
||||
/// a wallet refund here.
|
||||
pub async fn process_failed_job(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingGrokApiJob,
|
||||
reason: &str,
|
||||
) {
|
||||
let reason_lower = reason.to_lowercase();
|
||||
|
||||
let platform_rules_violation = reason_lower.contains("moderated")
|
||||
|| reason_lower.contains("moderation")
|
||||
|| reason_lower.contains("violates")
|
||||
|| reason_lower.contains("platform rules")
|
||||
|| reason_lower.contains("content policy");
|
||||
|
||||
let frontend_failure_category = if platform_rules_violation {
|
||||
Some(FrontendFailureCategory::ModelRulesViolation)
|
||||
} else {
|
||||
Some(FrontendFailureCategory::GenerationFailed)
|
||||
};
|
||||
|
||||
warn!(
|
||||
"Request for job {} failed: {}. Marking job failed.",
|
||||
job.job_token.as_str(), reason
|
||||
);
|
||||
|
||||
let mark_failed_result = mark_job_failed_by_token(MarkJobFailedByTokenArgs {
|
||||
pool: &deps.mysql_pool,
|
||||
job_token: &job.job_token,
|
||||
maybe_public_failure_reason: Some(reason),
|
||||
internal_debugging_failure_reason: reason,
|
||||
maybe_frontend_failure_category: frontend_failure_category,
|
||||
}).await;
|
||||
|
||||
if let Err(err) = mark_failed_result {
|
||||
error!(
|
||||
"Error marking job {} as failed: {:?}",
|
||||
job.job_token.as_str(), err
|
||||
);
|
||||
}
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
use anyhow::anyhow;
|
||||
use log::{error, info};
|
||||
|
||||
use bucket_paths::legacy::typified_paths::public::media_files::bucket_file_path::MediaFileBucketPath;
|
||||
use enums::by_table::generic_inference_jobs::inference_result_type::InferenceResultType;
|
||||
use enums::by_table::media_files::media_file_class::MediaFileClass;
|
||||
use enums::by_table::media_files::media_file_origin_category::MediaFileOriginCategory;
|
||||
use enums::by_table::media_files::media_file_origin_product_category::MediaFileOriginProductCategory;
|
||||
use enums::by_table::media_files::media_file_type::MediaFileType;
|
||||
use enums::common::generation_provider::GenerationProvider;
|
||||
use errors::AnyhowResult;
|
||||
use hashing::sha256::sha256_hash_bytes::sha256_hash_bytes;
|
||||
use mysql_queries::queries::generic_inference::api_providers::grok_api::list_pending_grok_api_jobs::PendingGrokApiJob;
|
||||
use mysql_queries::queries::generic_inference::web::mark_generic_inference_job_successfully_done_by_token::mark_generic_inference_job_successfully_done_by_token;
|
||||
use mysql_queries::queries::media_files::create::insert_builder::media_file_insert_builder::MediaFileInsertBuilder;
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::jobs::video_polling_job::alert_on_error::alert_pager_and_return_err;
|
||||
|
||||
const VIDEO_PREFIX: &str = "artcraft_";
|
||||
const VIDEO_SUFFIX: &str = ".mp4";
|
||||
|
||||
/// Download the completed video from xAI, upload to bucket, create media file
|
||||
/// record, and mark the job done.
|
||||
pub async fn process_successful_job(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingGrokApiJob,
|
||||
video_url: &str,
|
||||
) -> AnyhowResult<()> {
|
||||
|
||||
// --- Step 1: Download the video. ---
|
||||
|
||||
info!("Downloading video for job {} from: {}", job.job_token.as_str(), video_url);
|
||||
|
||||
let video_bytes: Vec<u8> = match reqwest::get(video_url).await {
|
||||
Ok(resp) => match resp.bytes().await {
|
||||
Ok(bytes) => bytes.to_vec(),
|
||||
Err(err) => {
|
||||
error!("Error reading video bytes for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"Grok API video download failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
error!("Error downloading video for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"Grok API video download failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
info!("Downloaded {} bytes for job {}", video_bytes.len(), job.job_token.as_str());
|
||||
|
||||
// --- Step 2: Hash and upload. ---
|
||||
|
||||
let checksum = sha256_hash_bytes(&video_bytes)
|
||||
.map_err(|err| anyhow!("error hashing video: {:?}", err))?;
|
||||
|
||||
let bucket_path = MediaFileBucketPath::generate_new(Some(VIDEO_PREFIX), Some(VIDEO_SUFFIX));
|
||||
let object_path = bucket_path.get_full_object_path_str();
|
||||
|
||||
info!("Uploading video to public bucket at path: {}", object_path);
|
||||
|
||||
let upload_result = deps
|
||||
.public_bucket_client
|
||||
.upload_file_with_content_type_process(object_path, &video_bytes, "video/mp4")
|
||||
.await;
|
||||
|
||||
if let Err(err) = upload_result {
|
||||
error!("Error uploading video for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"Grok API bucket upload failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
|
||||
info!("Uploaded video for job {}. Creating media file record.", job.job_token.as_str());
|
||||
|
||||
// --- Step 3: Create media file record. ---
|
||||
|
||||
let media_file_result = MediaFileInsertBuilder::new()
|
||||
.checksum_sha2(&checksum)
|
||||
.creator_ip_address(&job.creator_ip_address)
|
||||
.creator_set_visibility(job.creator_set_visibility)
|
||||
.file_size_bytes(video_bytes.len() as u64)
|
||||
.maybe_creator_anonymous_visitor(job.maybe_creator_anonymous_visitor_token.as_ref())
|
||||
.maybe_creator_user(job.maybe_creator_user_token.as_ref())
|
||||
.maybe_generation_provider(Some(GenerationProvider::Artcraft))
|
||||
.maybe_prompt_token(job.maybe_prompt_token.as_ref())
|
||||
.media_file_class(MediaFileClass::Video)
|
||||
.media_file_origin_category(MediaFileOriginCategory::Inference)
|
||||
.media_file_origin_product_category(MediaFileOriginProductCategory::VideoGeneration)
|
||||
.media_file_type(MediaFileType::Mp4)
|
||||
.mime_type("video/mp4")
|
||||
.public_bucket_directory_hash(&bucket_path)
|
||||
.insert_pool(&deps.mysql_pool)
|
||||
.await;
|
||||
|
||||
let media_file_token = match media_file_result {
|
||||
Ok(token) => token,
|
||||
Err(err) => {
|
||||
error!("Error inserting media file record for job {}: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"Grok API media file insert failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Created media file {} for job {}. Marking job complete.",
|
||||
media_file_token.as_str(), job.job_token.as_str()
|
||||
);
|
||||
|
||||
// --- Step 4: Mark job as done. ---
|
||||
|
||||
if let Err(err) = mark_generic_inference_job_successfully_done_by_token(
|
||||
&deps.mysql_pool,
|
||||
&job.job_token,
|
||||
Some(InferenceResultType::MediaFile),
|
||||
Some(media_file_token.as_str()),
|
||||
None,
|
||||
None,
|
||||
).await {
|
||||
error!("Error marking job {} done: {:?}", job.job_token.as_str(), err);
|
||||
return alert_pager_and_return_err(
|
||||
&deps.pager,
|
||||
"Grok API job completion update failed",
|
||||
err.into(),
|
||||
Some(job),
|
||||
);
|
||||
}
|
||||
|
||||
info!("Job {} completed successfully.", job.job_token.as_str());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
+180
@@ -0,0 +1,180 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use log::{error, info, warn};
|
||||
|
||||
use grok_api_client::api::requests::videos::video_status::video_status::{
|
||||
video_status, VideoStatusArgs, VideoStatusRequest, VideoStatusState, VideoStatusSuccess,
|
||||
};
|
||||
use grok_api_client::error::grok_error::GrokError;
|
||||
use grok_api_client::error::grok_specific_api_error::GrokSpecificApiError;
|
||||
use mysql_queries::queries::generic_inference::api_providers::grok_api::list_pending_grok_api_jobs::{
|
||||
list_pending_grok_api_jobs, PendingGrokApiJob,
|
||||
};
|
||||
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::jobs::video_polling_job::process_job::process_failed_job::process_failed_job;
|
||||
use crate::jobs::video_polling_job::process_job::process_successful_job::process_successful_job;
|
||||
|
||||
pub async fn video_polling_main_loop(job_dependencies: JobDependencies) {
|
||||
while !job_dependencies.application_shutdown.get() {
|
||||
let result = run_poll_iteration(&job_dependencies).await;
|
||||
|
||||
let sleep_millis = match result {
|
||||
Ok(_) => job_dependencies.poll_interval_success_millis,
|
||||
Err(err) => {
|
||||
error!("Error in Grok API poll iteration: {:?}", err);
|
||||
let _ = job_dependencies.job_stats.increment_failure_count();
|
||||
job_dependencies.poll_interval_failure_millis
|
||||
}
|
||||
};
|
||||
|
||||
tokio::select! {
|
||||
_ = tokio::time::sleep(Duration::from_millis(sleep_millis)) => {}
|
||||
_ = job_dependencies.shutdown_notify.notified() => {}
|
||||
}
|
||||
}
|
||||
|
||||
warn!("Grok API video polling main loop is shut down.");
|
||||
}
|
||||
|
||||
async fn run_poll_iteration(deps: &JobDependencies) -> anyhow::Result<()> {
|
||||
let pending_jobs = list_pending_grok_api_jobs(&deps.mysql_pool).await?;
|
||||
|
||||
if pending_jobs.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
info!("Found {} pending Grok API job(s).", pending_jobs.len());
|
||||
|
||||
for job in &pending_jobs {
|
||||
if deps.application_shutdown.get() {
|
||||
info!("Shutdown requested during Grok poll iteration. Stopping early.");
|
||||
break;
|
||||
}
|
||||
|
||||
poll_one_job(deps, job).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn poll_one_job(deps: &JobDependencies, job: &PendingGrokApiJob) {
|
||||
let poll_result = video_status(VideoStatusArgs {
|
||||
api_key: &deps.grok_api_key,
|
||||
request: VideoStatusRequest {
|
||||
request_id: job.request_id.clone(),
|
||||
},
|
||||
}).await;
|
||||
|
||||
let success = match poll_result {
|
||||
Ok(success) => success,
|
||||
Err(err) => {
|
||||
handle_video_status_error(deps, job, err).await;
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match success.state {
|
||||
VideoStatusState::Pending => {
|
||||
let progress = success.progress.map(|p| format!("{}%", p)).unwrap_or_else(|| "?".to_string());
|
||||
info!(
|
||||
"Grok request {} for job {} still pending ({} progress).",
|
||||
job.request_id, job.job_token.as_str(), progress,
|
||||
);
|
||||
}
|
||||
VideoStatusState::Done => {
|
||||
finalize_done_response(deps, job, success).await;
|
||||
}
|
||||
VideoStatusState::Failed => {
|
||||
// The Failed variant is mapped into `GrokError::ApiSpecific(VideoJobFailed)`
|
||||
// by the client. We shouldn't hit this branch, but treat defensively.
|
||||
warn!(
|
||||
"Unexpected VideoStatusState::Failed surfaced as Ok for request {} (job {}).",
|
||||
job.request_id, job.job_token.as_str(),
|
||||
);
|
||||
process_failed_job(deps, job, "Grok video status reported failed").await;
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn finalize_done_response(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingGrokApiJob,
|
||||
success: VideoStatusSuccess,
|
||||
) {
|
||||
let video_url = match success.video.as_ref().and_then(|v| v.url.clone()) {
|
||||
Some(url) => url,
|
||||
None => {
|
||||
warn!(
|
||||
"Grok request {} reported Done with no video.url for job {}. Skipping.",
|
||||
job.request_id, job.job_token.as_str(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!(
|
||||
"Grok request {} completed, processing job {}.",
|
||||
job.request_id, job.job_token.as_str(),
|
||||
);
|
||||
|
||||
match process_successful_job(deps, job, &video_url).await {
|
||||
Ok(()) => {
|
||||
let _ = deps.job_stats.increment_success_count();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"Error processing completed Grok request {} for job {}: {:?}",
|
||||
job.request_id, job.job_token.as_str(), err,
|
||||
);
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_video_status_error(
|
||||
deps: &JobDependencies,
|
||||
job: &PendingGrokApiJob,
|
||||
err: GrokError,
|
||||
) {
|
||||
match &err {
|
||||
// Terminal: mark the job failed.
|
||||
GrokError::ApiSpecific(GrokSpecificApiError::VideoJobFailed { code, message }) => {
|
||||
let reason = format!("Grok video failed ({}): {}", code, message);
|
||||
info!(
|
||||
"Grok request {} for job {} reported failed. Marking job failed.",
|
||||
job.request_id, job.job_token.as_str(),
|
||||
);
|
||||
process_failed_job(deps, job, &reason).await;
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
GrokError::ApiSpecific(GrokSpecificApiError::VideoJobExpired) => {
|
||||
let reason = "Grok video job expired";
|
||||
info!(
|
||||
"Grok request {} for job {} expired. Marking job failed.",
|
||||
job.request_id, job.job_token.as_str(),
|
||||
);
|
||||
process_failed_job(deps, job, reason).await;
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
GrokError::ApiSpecific(GrokSpecificApiError::NotFound) => {
|
||||
// Treat NotFound as terminal — xAI doesn't know about this request_id
|
||||
// anymore (expired retention or never existed).
|
||||
let reason = "Grok video job not found (likely expired)";
|
||||
info!(
|
||||
"Grok request {} for job {} not found. Marking job failed.",
|
||||
job.request_id, job.job_token.as_str(),
|
||||
);
|
||||
process_failed_job(deps, job, reason).await;
|
||||
let _ = deps.job_stats.increment_failure_count();
|
||||
}
|
||||
// Transient: just log and let the next iteration retry.
|
||||
_ => {
|
||||
warn!(
|
||||
"Transient error polling Grok request {} for job {}: {:?}",
|
||||
job.request_id, job.job_token.as_str(), err,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
// Never allow these
|
||||
#![forbid(private_bounds)]
|
||||
#![forbid(private_interfaces)]
|
||||
#![forbid(unused_must_use)]
|
||||
|
||||
// Always allow
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
#[macro_use] extern crate serde_derive;
|
||||
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use log::{info, warn};
|
||||
use sqlx::mysql::MySqlPoolOptions;
|
||||
use tokio::sync::Notify;
|
||||
|
||||
use bootstrap::bootstrap::{bootstrap, BootstrapArgs};
|
||||
use cloud_storage::bucket_client::BucketClient;
|
||||
use concurrency::relaxed_atomic_bool::RelaxedAtomicBool;
|
||||
use errors::AnyhowResult;
|
||||
use grok_api_client::creds::grok_api_key::GrokApiKey;
|
||||
use jobs_common::job_stats::JobStats;
|
||||
use server_environment::ServerEnvironment;
|
||||
use shared_env_var_config::logging::DEFAULT_RUST_LOG;
|
||||
use shared_env_var_config::mysql::env_get_mysql_connection_string_or_default;
|
||||
|
||||
use crate::http_server::run_http_server::{launch_http_server, CreateServerArgs};
|
||||
use crate::job_dependencies::JobDependencies;
|
||||
use crate::jobs::image_generation_job::image_generation_main_loop::image_generation_main_loop;
|
||||
use crate::jobs::video_polling_job::video_polling_main_loop::video_polling_main_loop;
|
||||
use crate::startup::build_pager::build_pager;
|
||||
|
||||
pub mod http_server;
|
||||
pub mod job_dependencies;
|
||||
pub mod jobs;
|
||||
pub mod startup;
|
||||
|
||||
// Bucket config
|
||||
const ENV_ACCESS_KEY: &str = "ACCESS_KEY";
|
||||
const ENV_SECRET_KEY: &str = "SECRET_KEY";
|
||||
const ENV_REGION_NAME: &str = "REGION_NAME";
|
||||
const ENV_PUBLIC_BUCKET_NAME: &str = "PUBLIC_BUCKET_NAME";
|
||||
const ENV_S3_ENDPOINT: &str = "S3_COMPATIBLE_ENDPOINT_URL";
|
||||
const ENV_GROK_API_KEY: &str = "GROK_API_KEY";
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> AnyhowResult<()> {
|
||||
|
||||
let container_environment = bootstrap(BootstrapArgs {
|
||||
app_name: "grok-api-job",
|
||||
default_logging_override: Some(DEFAULT_RUST_LOG),
|
||||
config_search_directories: &[".", "./config", "crates/service/job/grok_api_job/config"],
|
||||
ignore_legacy_dot_env_file: true,
|
||||
})?;
|
||||
|
||||
info!("Hostname: {}", &container_environment.hostname);
|
||||
|
||||
let _k8s_node_name = easyenv::get_env_string_optional("K8S_NODE_NAME");
|
||||
let _k8s_pod_name = easyenv::get_env_string_optional("K8S_POD_NAME");
|
||||
|
||||
let db_connection_string = env_get_mysql_connection_string_or_default();
|
||||
|
||||
info!("Connecting to database...");
|
||||
|
||||
let mysql_pool = MySqlPoolOptions::new()
|
||||
.max_connections(2)
|
||||
.connect(&db_connection_string)
|
||||
.await?;
|
||||
|
||||
info!("Connected to MySQL.");
|
||||
|
||||
let server_environment = ServerEnvironment::from_str(
|
||||
&easyenv::get_env_string_required("SERVER_ENVIRONMENT")?,
|
||||
)
|
||||
.ok_or(anyhow!("invalid server environment"))?;
|
||||
|
||||
// Bucket setup
|
||||
let access_key = easyenv::get_env_string_required(ENV_ACCESS_KEY)?;
|
||||
let secret_key = easyenv::get_env_string_required(ENV_SECRET_KEY)?;
|
||||
let region_name = easyenv::get_env_string_required(ENV_REGION_NAME)?;
|
||||
let public_bucket_name = easyenv::get_env_string_required(ENV_PUBLIC_BUCKET_NAME)?;
|
||||
let s3_compatible_endpoint_url = easyenv::get_env_string_required(ENV_S3_ENDPOINT)?;
|
||||
|
||||
let bucket_timeout = easyenv::get_env_duration_seconds_or_default(
|
||||
"BUCKET_TIMEOUT_SECONDS",
|
||||
Duration::from_secs(60 * 5),
|
||||
);
|
||||
|
||||
let public_bucket_client = BucketClient::create(
|
||||
&access_key,
|
||||
&secret_key,
|
||||
®ion_name,
|
||||
&public_bucket_name,
|
||||
&s3_compatible_endpoint_url,
|
||||
None,
|
||||
Some(bucket_timeout),
|
||||
)?;
|
||||
|
||||
// Grok API key
|
||||
let grok_api_key_str = easyenv::get_env_string_required(ENV_GROK_API_KEY)?;
|
||||
let grok_api_key = GrokApiKey::new(grok_api_key_str);
|
||||
|
||||
// How often to poll after a successful iteration (default: 5 seconds)
|
||||
let poll_interval_success_millis: u64 = easyenv::get_env_num(
|
||||
"GROK_POLL_INTERVAL_SUCCESS_MILLIS",
|
||||
5_000,
|
||||
)?;
|
||||
|
||||
// How often to poll after a failed iteration (default: 15 seconds)
|
||||
let poll_interval_failure_millis: u64 = easyenv::get_env_num(
|
||||
"GROK_POLL_INTERVAL_FAILURE_MILLIS",
|
||||
15_000,
|
||||
)?;
|
||||
|
||||
// Stub image_generation_job tick interval (default: 60 seconds).
|
||||
let image_generation_poll_interval_millis: u64 = easyenv::get_env_num(
|
||||
"GROK_IMAGE_POLL_INTERVAL_MILLIS",
|
||||
60_000,
|
||||
)?;
|
||||
|
||||
let application_shutdown = RelaxedAtomicBool::new(false);
|
||||
let shutdown_notify = Arc::new(Notify::new());
|
||||
let job_stats = JobStats::new();
|
||||
|
||||
// Pager setup
|
||||
let (pager, pager_worker) = build_pager(server_environment, &container_environment.hostname);
|
||||
|
||||
info!("Spawning pager worker.");
|
||||
|
||||
// NB: The pager worker uses Condvar::wait() which is a blocking syscall.
|
||||
// It must run on a dedicated OS thread, not a tokio task, to avoid blocking
|
||||
// the tokio runtime.
|
||||
std::thread::spawn(move || {
|
||||
let rt = tokio::runtime::Runtime::new().expect("pager worker tokio runtime");
|
||||
rt.block_on(pager_worker.run());
|
||||
});
|
||||
|
||||
let pager_for_shutdown = pager.clone();
|
||||
|
||||
let create_server_args = CreateServerArgs {
|
||||
container_environment: container_environment.clone(),
|
||||
job_stats: job_stats.clone(),
|
||||
};
|
||||
|
||||
let job_dependencies = JobDependencies {
|
||||
mysql_pool,
|
||||
public_bucket_client,
|
||||
grok_api_key,
|
||||
server_environment,
|
||||
pager,
|
||||
job_stats,
|
||||
poll_interval_success_millis,
|
||||
poll_interval_failure_millis,
|
||||
image_generation_poll_interval_millis,
|
||||
application_shutdown: application_shutdown.clone(),
|
||||
shutdown_notify: shutdown_notify.clone(),
|
||||
};
|
||||
|
||||
// HTTP server runs on a separate OS thread with its own actix System.
|
||||
std::thread::spawn(move || {
|
||||
let actix_runtime = actix_web::rt::System::new();
|
||||
let http_server_handle = launch_http_server(create_server_args);
|
||||
|
||||
actix_runtime.block_on(http_server_handle)
|
||||
.expect("HTTP server should not exit.");
|
||||
|
||||
warn!("HTTP server thread is shut down.");
|
||||
});
|
||||
|
||||
// Listen for SIGTERM / Ctrl-C to trigger graceful shutdown.
|
||||
let application_shutdown_for_signal = application_shutdown.clone();
|
||||
let shutdown_notify_for_signal = shutdown_notify.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
match tokio::signal::ctrl_c().await {
|
||||
Ok(()) => {
|
||||
info!("Received shutdown signal. Shutting down...");
|
||||
application_shutdown_for_signal.set(true);
|
||||
shutdown_notify_for_signal.notify_waiters();
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Error listening for shutdown signal: {:?}", err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Spawn all polling loops as concurrent tasks.
|
||||
let video_deps = job_dependencies.clone();
|
||||
let image_deps = job_dependencies;
|
||||
|
||||
let video_handle = tokio::spawn(async move {
|
||||
video_polling_main_loop(video_deps).await;
|
||||
});
|
||||
|
||||
let image_handle = tokio::spawn(async move {
|
||||
image_generation_main_loop(image_deps).await;
|
||||
});
|
||||
|
||||
let _ = tokio::join!(video_handle, image_handle);
|
||||
|
||||
info!("Shutting down pager worker...");
|
||||
pager_for_shutdown.shutdown_worker();
|
||||
|
||||
info!("Grok API job exiting.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
use log::{info, warn};
|
||||
use pager::client::pager::Pager;
|
||||
use pager::client::pager_builder::PagerBuilder;
|
||||
use pager::worker::pager_worker::PagerWorker;
|
||||
use rootly_client::creds::rootly_api_key::RootlyApiKey;
|
||||
use rootly_config::services::ROOTLY_SERVICE_ID_SEEDANCE2_PRO_JOB;
|
||||
use rootly_config::urgencies::{ROOTLY_URGENCY_ID_HIGH, ROOTLY_URGENCY_ID_LOW, ROOTLY_URGENCY_ID_MEDIUM};
|
||||
use shared_env_var_config::paging::{env_enable_paging_default_false, env_optional_rootly_api_key, env_optional_rootly_notification_target_id, env_optional_rootly_notification_target_type};
|
||||
|
||||
pub fn build_pager(
|
||||
server_environment: server_environment::ServerEnvironment,
|
||||
hostname: &str,
|
||||
) -> (Pager, PagerWorker) {
|
||||
let is_paging_enabled = env_enable_paging_default_false();
|
||||
|
||||
info!("Paging enabled: {}", is_paging_enabled);
|
||||
|
||||
let environment = if server_environment.is_deployed_in_production() {
|
||||
"production"
|
||||
} else {
|
||||
"development"
|
||||
};
|
||||
|
||||
let builder = PagerBuilder::new()
|
||||
.application_name("grok-api-job".to_string())
|
||||
.environment(environment.to_string())
|
||||
.hostname(hostname.to_string())
|
||||
.service_id(ROOTLY_SERVICE_ID_SEEDANCE2_PRO_JOB.to_string());
|
||||
|
||||
if !is_paging_enabled {
|
||||
warn!("ENABLE_PAGING is false. Pager will be NoOp.");
|
||||
return builder.build_with_worker();
|
||||
}
|
||||
|
||||
let maybe_api_key = env_optional_rootly_api_key();
|
||||
|
||||
match maybe_api_key {
|
||||
Some(api_key) => {
|
||||
info!("Rootly API key found. Configuring pager with Rootly backend.");
|
||||
build_rootly_pager(builder, api_key)
|
||||
}
|
||||
None => {
|
||||
warn!("ROOTLY_API_KEY not set. Pager will not send real pages.");
|
||||
builder.build_with_worker()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_rootly_pager(builder: PagerBuilder, api_key: String) -> (Pager, PagerWorker) {
|
||||
let mut rootly_builder = builder
|
||||
.rootly(RootlyApiKey::new(api_key))
|
||||
.urgency_id_high(ROOTLY_URGENCY_ID_HIGH.to_string())
|
||||
.urgency_id_medium(ROOTLY_URGENCY_ID_MEDIUM.to_string())
|
||||
.urgency_id_low(ROOTLY_URGENCY_ID_LOW.to_string());
|
||||
|
||||
let target_type = env_optional_rootly_notification_target_type();
|
||||
let target_id = env_optional_rootly_notification_target_id();
|
||||
|
||||
if let (Some(t_type), Some(t_id)) = (target_type, target_id) {
|
||||
rootly_builder = rootly_builder.notification_target(t_type, t_id);
|
||||
}
|
||||
|
||||
rootly_builder.build_with_worker()
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub mod build_pager;
|
||||
@@ -55,6 +55,7 @@ error_utils.workspace = true
|
||||
errors.workspace = true
|
||||
fal_client.workspace = true
|
||||
gmicloud_client.workspace = true
|
||||
grok_api_client.workspace = true
|
||||
filesys.workspace = true
|
||||
google_sign_in = { path = "../../../api_clients/google_sign_in" }
|
||||
hashing = { path = "../../../lib/files/hashing" }
|
||||
|
||||
@@ -19,6 +19,42 @@ pub const OMNI_GEN_VIDEO_MODELS_AND_PROVIDERS: Lazy<OmniGenVideoModelsResponse>
|
||||
fn build_omni_gen_video_models() -> Vec<OmniGenVideoModelDetails> {
|
||||
let mut models = Vec::new();
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::GrokImagineVideo,
|
||||
model_creator: Some(ModelCreator::Grok),
|
||||
full_name: Some("Grok Imagine".to_string()),
|
||||
text_prompt_supported: Some(true),
|
||||
starting_keyframe_supported: Some(true),
|
||||
ending_keyframe_supported: Some(false),
|
||||
image_references_supported: Some(true),
|
||||
image_references_max: Some(7),
|
||||
//video_references_supported: Some(false),
|
||||
//video_references_max: Some(3),
|
||||
//video_references_max_total_duration_seconds: Some(15),
|
||||
aspect_ratio_options: Some(vec![
|
||||
CommonAspectRatio::WideSixteenByNine,
|
||||
CommonAspectRatio::WideFourByThree,
|
||||
CommonAspectRatio::WideThreeByTwo,
|
||||
CommonAspectRatio::Square,
|
||||
CommonAspectRatio::TallTwoByThree,
|
||||
CommonAspectRatio::TallThreeByFour,
|
||||
CommonAspectRatio::TallNineBySixteen,
|
||||
]),
|
||||
aspect_ratio_default: Some(CommonAspectRatio::WideSixteenByNine),
|
||||
resolution_options: Some(vec![
|
||||
CommonResolution::FourEightyP,
|
||||
CommonResolution::SevenTwentyP,
|
||||
]),
|
||||
resolution_default: Some(CommonResolution::SevenTwentyP),
|
||||
//batch_size_options: Some(vec![1, 2, 4]),
|
||||
//batch_size_default: Some(1),
|
||||
duration_seconds_min: Some(1),
|
||||
duration_seconds_max: Some(15),
|
||||
duration_seconds_max_with_image_references: Some(10),
|
||||
duration_seconds_default: Some(8),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
models.push(OmniGenVideoModelDetails {
|
||||
model: CommonVideoModel::HappyHorse1p0,
|
||||
model_creator: Some(ModelCreator::Alibaba),
|
||||
|
||||
+2
@@ -142,6 +142,8 @@ fn map_image_model(model: CommonImageModel) -> Result<RouterImageModel, HandlerE
|
||||
CommonImageModel::Seedream4 => RouterImageModel::Seedream4,
|
||||
CommonImageModel::Seedream4p5 => RouterImageModel::Seedream4p5,
|
||||
CommonImageModel::Seedream5Lite => RouterImageModel::Seedream5Lite,
|
||||
CommonImageModel::GrokImagineImage => RouterImageModel::GrokImagineImage,
|
||||
CommonImageModel::GrokImagineImageQuality => RouterImageModel::GrokImagineImageQuality,
|
||||
};
|
||||
Ok(router_model)
|
||||
}
|
||||
|
||||
+1
@@ -130,6 +130,7 @@ fn map_provider(
|
||||
fn map_video_model(model: CommonVideoModel) -> Result<RouterVideoModel, HandlerError> {
|
||||
let router_model = match model {
|
||||
CommonVideoModel::GrokVideo => RouterVideoModel::GrokVideo,
|
||||
CommonVideoModel::GrokImagineVideo => RouterVideoModel::GrokImagineVideo,
|
||||
CommonVideoModel::Kling16Pro => RouterVideoModel::Kling16Pro,
|
||||
CommonVideoModel::Kling21Pro => RouterVideoModel::Kling21Pro,
|
||||
CommonVideoModel::Kling21Master => RouterVideoModel::Kling21Master,
|
||||
|
||||
+6
@@ -4,6 +4,7 @@ use artcraft_router::api::provider::Provider;
|
||||
use artcraft_router::client::router_client::RouterClient;
|
||||
use artcraft_router::client::router_fal_client::RouterFalClient;
|
||||
use artcraft_router::client::router_gmicloud_client::RouterGmiCloudClient;
|
||||
use artcraft_router::client::router_grok_api_client::RouterGrokApiClient;
|
||||
use artcraft_router::client::router_seedance2pro_client::RouterSeedance2ProClient;
|
||||
use seedance2pro_client::creds::seedance2pro_session::Seedance2ProSession;
|
||||
|
||||
@@ -31,6 +32,11 @@ pub fn build_router_client(
|
||||
server_state.gmicloud.api_key.clone(),
|
||||
)))
|
||||
}
|
||||
Provider::GrokApi => {
|
||||
Ok(RouterClient::GrokApi(RouterGrokApiClient::new(
|
||||
server_state.grok_api.api_key.clone(),
|
||||
)))
|
||||
}
|
||||
other => {
|
||||
Err(AdvancedCommonWebError::server_error_with_message(
|
||||
&format!("Unsupported provider for video generation: {:?}", other),
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
use log::warn;
|
||||
|
||||
use mysql_queries::queries::generic_inference::api_providers::grok_api::insert_generic_inference_job_for_grok_api::{
|
||||
insert_generic_inference_job_for_grok_api, InsertGrokApiInferenceJobArgs,
|
||||
};
|
||||
use tokens::tokens::generic_inference_jobs::InferenceJobToken;
|
||||
|
||||
use crate::http_server::common_responses::advanced_common_web_error::AdvancedCommonWebError;
|
||||
|
||||
use super::shared_job_args::SharedJobArgs;
|
||||
|
||||
pub struct InsertGrokApiJobArgs<'a, 'tx> {
|
||||
pub external_request_id: &'a str,
|
||||
pub shared: SharedJobArgs<'a, 'tx>,
|
||||
}
|
||||
|
||||
pub async fn insert_grok_api_job(args: InsertGrokApiJobArgs<'_, '_>) -> Result<InferenceJobToken, AdvancedCommonWebError> {
|
||||
let InsertGrokApiJobArgs {
|
||||
external_request_id,
|
||||
shared,
|
||||
} = args;
|
||||
|
||||
let db_result = insert_generic_inference_job_for_grok_api(
|
||||
InsertGrokApiInferenceJobArgs {
|
||||
apriori_job_token: shared.apriori_job_token,
|
||||
uuid_idempotency_token: shared.idempotency_token,
|
||||
external_request_id,
|
||||
maybe_model_type: shared.maybe_model_type,
|
||||
maybe_prompt_token: shared.maybe_prompt_token,
|
||||
maybe_creator_user_token: Some(shared.user_token),
|
||||
maybe_avt_token: shared.maybe_avt_token,
|
||||
creator_ip_address: shared.ip_address,
|
||||
maybe_debug_log_event_token: shared.maybe_debug_log_event_token,
|
||||
mysql_executor: &mut **shared.transaction,
|
||||
phantom: Default::default(),
|
||||
}
|
||||
).await;
|
||||
|
||||
match db_result {
|
||||
Ok(token) => Ok(token),
|
||||
Err(err) => {
|
||||
warn!("Error inserting Grok (xAI) API inference job: {:?}", err);
|
||||
Err(AdvancedCommonWebError::from_error(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
pub mod insert_fal_job;
|
||||
pub mod insert_gmicloud_job;
|
||||
pub mod insert_grok_api_job;
|
||||
pub mod insert_seedance2pro_jobs;
|
||||
pub mod shared_job_args;
|
||||
|
||||
+41
@@ -34,6 +34,7 @@ use crate::http_server::endpoints::generate::common::payments_error_test::paymen
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_fal_job::{insert_fal_job, InsertFalJobArgs};
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_gmicloud_job::{insert_gmicloud_job, InsertGmiCloudJobArgs};
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_grok_api_job::{insert_grok_api_job, InsertGrokApiJobArgs};
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::insert_seedance2pro_jobs::{insert_seedance2pro_jobs, InsertSeedance2proJobsArgs};
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::insert_db_job::shared_job_args::SharedJobArgs;
|
||||
use crate::http_server::endpoints::omni_gen::generate::video::pipeline_v1::run_pipeline_v1::{run_pipeline_v1, RunPipelineV1Args};
|
||||
@@ -193,6 +194,7 @@ pub async fn omni_gen_video_generate_handler(
|
||||
Some(CommonVideoModel::Seedance2p0BytePlusFast) => true,
|
||||
Some(CommonVideoModel::PreviewModel) => true,
|
||||
Some(CommonVideoModel::PreviewModelFast) => true,
|
||||
Some(CommonVideoModel::GrokImagineVideo) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -258,6 +260,23 @@ pub async fn omni_gen_video_generate_handler(
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== DEBUG LOG: GROK API REQUEST ==================== //
|
||||
|
||||
if let GenerateVideoResponse::Grok(ref grok_payload) = pipeline_result.response {
|
||||
if let Some(ref outbound_request) = grok_payload.maybe_outbound_request {
|
||||
if let Err(err) = insert_debug_log(InsertDebugLogArgs {
|
||||
apriori_debug_log_event_token: Some(&debug_log_event_token),
|
||||
maybe_creator_user_token: Some(user_token),
|
||||
debug_log_type: DebugLogType::GrokApiRequest,
|
||||
message: &format!("{:#?}", outbound_request),
|
||||
mysql_executor: &mut *mysql_connection,
|
||||
phantom: Default::default(),
|
||||
}).await {
|
||||
warn!("Failed to insert Grok API request debug log: {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== WRITE RESULT ==================== //
|
||||
|
||||
let ip_address = get_request_ip(&http_request);
|
||||
@@ -418,6 +437,28 @@ pub async fn omni_gen_video_generate_handler(
|
||||
vec![token],
|
||||
)
|
||||
}
|
||||
GenerateVideoResponse::Grok(payload) => {
|
||||
info!("Inserting Grok (xAI) API job with token: {:?}", pipeline_result.billing.apriori_job_token);
|
||||
let token = insert_grok_api_job(InsertGrokApiJobArgs {
|
||||
external_request_id: &payload.request_id,
|
||||
shared: SharedJobArgs {
|
||||
apriori_job_token: &pipeline_result.billing.apriori_job_token,
|
||||
idempotency_token: &idempotency_token,
|
||||
user_token,
|
||||
maybe_avt_token: maybe_avt_token.as_ref(),
|
||||
maybe_model_type: request.model.map(|v| v.to_common_model_type()),
|
||||
maybe_prompt_token: prompt_token.as_ref(),
|
||||
maybe_debug_log_event_token: Some(&debug_log_event_token),
|
||||
ip_address: &ip_address,
|
||||
transaction: &mut transaction,
|
||||
},
|
||||
}).await?;
|
||||
|
||||
(
|
||||
token.clone(),
|
||||
vec![token],
|
||||
)
|
||||
}
|
||||
other => {
|
||||
error!("Unexpected generation response variant: {:?}", other);
|
||||
return Err(AdvancedCommonWebError::server_error_with_message("Unexpected generation response"));
|
||||
|
||||
+3
-2
@@ -61,6 +61,7 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
CommonVideoModel::Seedance2p0Fast => Provider::Seedance2Pro,
|
||||
CommonVideoModel::Seedance2p0Ultra => Provider::GmiCloud,
|
||||
CommonVideoModel::Seedance2p0UltraFast => Provider::GmiCloud,
|
||||
CommonVideoModel::GrokImagineVideo => Provider::GrokApi,
|
||||
_ => Provider::Fal,
|
||||
};
|
||||
|
||||
@@ -68,9 +69,9 @@ pub async fn run_pipeline_v2(args: RunPipelineV2Args<'_>) -> Result<PipelineResu
|
||||
let mut exec_builder = router_builder.clone();
|
||||
exec_builder.provider = provider;
|
||||
|
||||
// GmiCloud takes URLs directly (like Fal), not media file tokens.
|
||||
// GmiCloud and Grok (xAI) take URLs directly (like Fal), not media file tokens.
|
||||
// Resolve tokens to URLs before building.
|
||||
if matches!(provider, Provider::GmiCloud) {
|
||||
if matches!(provider, Provider::GmiCloud | Provider::GrokApi) {
|
||||
resolve_media_tokens_to_urls(&mut exec_builder, media_file_to_url_map.as_ref());
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ use crate::state::certs::google_sign_in_cert::GoogleSignInCert;
|
||||
use crate::state::memory_cache::model_token_to_info_cache::ModelTokenToInfoCache;
|
||||
use crate::state::server_state::{
|
||||
BeebleData, DurableInMemoryCaches, EnvConfig, EphemeralInMemoryCaches, FalData,
|
||||
GmiCloudData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo,
|
||||
GmiCloudData, GrokApiData, InMemoryCaches, OpenAiData, ResendData, Seedance2ProData, ServerInfo,
|
||||
ServerState, TrollBans, WorldLabsData,
|
||||
};
|
||||
use crate::threads::db_health_checker_thread::db_health_check_status::HealthCheckStatus;
|
||||
@@ -203,6 +203,7 @@ pub async fn setup_dependencies(server_hostname: &str) -> AnyhowResult<SetupResu
|
||||
let openai_api_key = easyenv::get_env_string_required("OPENAI_API_KEY")?;
|
||||
let resend_api_key = easyenv::get_env_string_required("RESEND_API_KEY")?;
|
||||
let gmicloud_api_key = easyenv::get_env_string_required("GMICLOUD_API_KEY")?;
|
||||
let grok_api_key = easyenv::get_env_string_required("GROK_API_KEY")?;
|
||||
let worldlabs_api_key = easyenv::get_env_string_required("WORLDLABS_API_KEY")?;
|
||||
|
||||
let startup_time = Utc::now();
|
||||
@@ -257,6 +258,9 @@ pub async fn setup_dependencies(server_hostname: &str) -> AnyhowResult<SetupResu
|
||||
gmicloud: GmiCloudData {
|
||||
api_key: gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey::new(gmicloud_api_key),
|
||||
},
|
||||
grok_api: GrokApiData {
|
||||
api_key: grok_api_client::creds::grok_api_key::GrokApiKey::new(grok_api_key),
|
||||
},
|
||||
seedance2pro: Seedance2ProData {
|
||||
cookies: easyenv::get_env_string_required("SEEDANCE2PRO_COOKIES")?,
|
||||
cookies_byteplus: easyenv::get_env_string_required("SEEDANCE2PRO_WHITELIST_COOKIES")?,
|
||||
|
||||
@@ -93,6 +93,8 @@ pub struct ServerState {
|
||||
|
||||
pub gmicloud: GmiCloudData,
|
||||
|
||||
pub grok_api: GrokApiData,
|
||||
|
||||
pub beeble: BeebleData,
|
||||
|
||||
pub seedance2pro: Seedance2ProData,
|
||||
@@ -324,6 +326,12 @@ pub struct GmiCloudData {
|
||||
pub api_key: gmicloud_client::creds::gmicloud_api_key::GmiCloudApiKey,
|
||||
}
|
||||
|
||||
/// Grok (xAI) API integration
|
||||
#[derive(Clone)]
|
||||
pub struct GrokApiData {
|
||||
pub api_key: grok_api_client::creds::grok_api_key::GrokApiKey,
|
||||
}
|
||||
|
||||
/// Beeble SwitchX integration
|
||||
#[derive(Clone)]
|
||||
pub struct BeebleData {
|
||||
|
||||
@@ -19,7 +19,7 @@ if (import.meta.env.DEV) {
|
||||
window.location.origin,
|
||||
);
|
||||
// NB: This is for Brandon to test with storyteller-web locally:
|
||||
//StorytellerApiHostStore.getInstance().setDevelopment();
|
||||
StorytellerApiHostStore.getInstance().setDevelopment();
|
||||
} catch (e) {
|
||||
console.warn("Failed to set dev API host override", e);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user