Compare commits

...
Author SHA1 Message Date
Brandon Thomas fd8fe7f3db fix tests 2026-07-08 16:17:12 -04:00
Brandon Thomas 54b172bf6f test on api client 2026-07-08 16:08:44 -04:00
Brandon Thomas d7868c7da0 don't validate cost estimates 2026-07-08 15:44:56 -04:00
14 changed files with 283 additions and 115 deletions
@@ -2,7 +2,7 @@ use serde_derive::{Deserialize, Serialize};
use utoipa::ToSchema;
/// Response body for the omni-gen video cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct OmniGenVideoCostResponse {
pub success: bool,
@@ -1,7 +1,6 @@
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::generate_video_request_builder::GenerateVideoRequestBuilder;
use crate::generate::generate_video::providers::artcraft::build_common::{
build_artcraft_omni_video_request, SupportedResolutions, UltraWideSupport,
@@ -27,17 +26,10 @@ pub fn build_artcraft_grok_imagine_video_1p5(
UltraWideSupport::Unsupported,
)?;
// xAI's v1.5 model rejects text-to-video at the server. Bounce here rather
// than spending an upstream call to learn the same thing.
if request.start_frame_image_media_token.is_none()
&& request.reference_image_media_tokens.as_ref().map_or(true, |v| v.is_empty())
{
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "text-to-video isn't supported by grok-imagine-video-1.5; supply a start_frame or at least one reference image".to_string(),
}));
}
// NB: xAI's v1.5 model rejects text-to-video at the server, but that is
// NOT enforced here — the cost path builds image-less requests to price
// them while the user is still composing. The generate endpoints reject
// image-less v1.5 requests at the handler layer (validate_when_image_required).
let state = ArtcraftGrokImagineVideo1p5RequestState { request };
Ok(VideoGenerationDraftOrRequest::Request(
@@ -129,6 +121,15 @@ mod tests {
mod media_token_tests {
use super::*;
#[test]
fn builds_without_any_image_inputs() {
// The cost path prices image-less requests while the user is still
// composing; the generate endpoints enforce the image requirement.
let req = unwrap_request(make_builder(|_| {}));
assert!(req.request.start_frame_image_media_token.is_none());
assert!(req.request.reference_image_media_tokens.is_none());
}
#[test]
fn start_frame_token_passed_through() {
let token = MediaFileToken::new("mf_start".to_string());
@@ -180,12 +181,6 @@ mod tests {
fn make_builder(f: impl FnOnce(&mut GenerateVideoRequestBuilder)) -> GenerateVideoRequestBuilder {
let mut builder = base_builder();
f(&mut builder);
// v1.5 requires an input image; tests that don't explicitly exercise the
// image-handling paths get a default MediaFileToken start_frame so they
// sail past the no-image guard in `build()`.
if builder.start_frame.is_none() && builder.reference_images.is_none() {
builder.start_frame = Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_default".to_string())));
}
builder
}
@@ -9,7 +9,6 @@ use grok_api_client::api::types::video_types::video_model::VideoModel as GrokVid
use grok_api_client::api::types::video_types::video_resolution::VideoResolution as GrokResolution;
use crate::errors::artcraft_router_error::ArtcraftRouterError;
use crate::errors::client_error::ClientError;
use crate::generate::generate_video::video_generation_cost_estimate::VideoGenerationCostEstimate;
use crate::generate::generate_video::providers::artcraft::grok_imagine_video_1p5::request::ArtcraftGrokImagineVideo1p5RequestState;
@@ -65,16 +64,10 @@ impl ArtcraftGrokImagineVideo1p5CostState {
}
pub fn estimate_cost(&self) -> Result<VideoGenerationCostEstimate, ArtcraftRouterError> {
// Defense in depth: `build()` already enforces this, but a state
// constructed by hand must not get a quote for an operation xAI will
// reject.
if self.input_image_count == 0 {
return Err(ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption {
field: "image_inputs",
value: "text-to-video isn't supported by grok-imagine-video-1.5; supply a start_frame or at least one reference image".to_string(),
}));
}
// NB: Image-less requests DO get a quote even though xAI's v1.5 rejects
// text-to-video — the cost path prices requests the user is still
// composing. The image requirement is enforced at send time (request.rs)
// and by the generate endpoints.
let base = self.base_cost_in_cents_for_batch();
// 5% markup, ceil so the user is always charged enough to cover the
// upstream grok cost regardless of rounding.
@@ -204,24 +197,23 @@ mod tests {
assert_eq!(state.estimate_cost().expect("estimate_cost").cost_in_usd_cents.unwrap(), 150);
}
/// v1.5 doesn't support text-to-video. The cost calculator must refuse
/// to quote an image-less request even if a state is constructed
/// directly.
/// v1.5 doesn't support text-to-video at generation time, but the cost
/// calculator still quotes an image-less request — the cost UI prices
/// requests before the user has attached an image. The requirement is
/// enforced at send time and by the generate endpoints.
#[test]
fn estimate_cost_rejects_text_only_request() {
fn estimate_cost_quotes_text_only_request() {
let state = ArtcraftGrokImagineVideo1p5CostState {
resolution: CommonResolution::SevenTwentyP,
duration_seconds: 5,
batch_count: 1,
input_image_count: 0,
};
let err = state.estimate_cost().expect_err("text-only should be rejected");
match err {
ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field, .. }) => {
assert_eq!(field, "image_inputs");
}
other => panic!("expected Client(ModelDoesNotSupportOption), got {:?}", other),
}
let base = state.base_cost_in_cents_for_batch();
let want = base.saturating_mul(MARKUP_NUMERATOR).div_ceil(MARKUP_DENOMINATOR);
let got = state.estimate_cost().expect("estimate_cost").cost_in_usd_cents.unwrap();
assert_eq!(got, want);
assert!(got > 0);
}
// (resolution, duration, image_count, batch) — every case has at least
@@ -326,6 +318,28 @@ mod tests {
.cost_in_usd_cents.unwrap();
assert!(p1080 > p720, "1080p ({p1080}) should cost more than 720p ({p720})");
}
/// Regression: an image-less v1.5 request must get a quote through the
/// full public builder path (build2 → estimate_cost). The cost UI polls
/// for a price before the user has attached an image; the image
/// requirement is enforced at send time and by the generate endpoints,
/// NOT here.
#[test]
fn image_less_request_gets_a_quote_through_the_builder() {
let builder = GenerateVideoRequestBuilder {
model: RouterVideoModel::GrokImagineVideo1p5,
provider: RouterProvider::Artcraft,
duration_seconds: Some(5),
video_batch_count: Some(1),
..Default::default()
};
let cost = builder.build2()
.expect("build2 should succeed without an image")
.estimate_cost()
.expect("estimate_cost should succeed without an image");
assert!(cost.cost_in_usd_cents.unwrap() > 0);
assert_eq!(cost.cost_in_credits, cost.cost_in_usd_cents);
}
}
// ── Helpers ────────────────────────────────────────────────────────────
@@ -390,8 +404,7 @@ mod tests {
resolution,
duration_seconds: Some(duration_seconds),
video_batch_count: Some(video_batch_count),
// v1.5 requires an input image — the no-image guard in build()
// rejects T2V.
// These cases price the image-to-video tier (one input image).
start_frame: Some(ImageRef::MediaFileToken(MediaFileToken::new("mf_default".to_string()))),
..Default::default()
};
@@ -13,8 +13,11 @@ pub struct ArtcraftGrokImagineVideo1p5RequestState {
impl ArtcraftGrokImagineVideo1p5RequestState {
pub async fn send(&self, client: &RouterArtcraftClient) -> Result<GenerateVideoResponse, ArtcraftRouterError> {
// Defense in depth: `build()` already enforces this. Bouncing here costs
// nothing and avoids an upstream call we know will fail.
// xAI's v1.5 model rejects text-to-video at the server. This is the
// GENERATION-time guard — `build()` and `estimate_cost()` deliberately
// allow image-less states so the cost path can quote a request the user
// is still composing. Bouncing here costs nothing and avoids an upstream
// call we know will fail.
if self.request.start_frame_image_media_token.is_none()
&& self.request.reference_image_media_tokens.as_ref().map_or(true, |v| v.is_empty())
{
@@ -27,3 +30,55 @@ impl ArtcraftGrokImagineVideo1p5RequestState {
send_artcraft_omni_video_request(&self.request, client).await
}
}
#[cfg(test)]
mod tests {
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
use artcraft_client::utils::api_host::ApiHost;
use enums::common::generation::common_video_model::CommonVideoModel as CommonVideoModelEnum;
use super::*;
/// Generation must never reach xAI with an image-less v1.5 request. The
/// build/cost layers allow that state (the cost path quotes it), so the
/// send-time guard is the router's last line of defense.
#[tokio::test]
async fn send_rejects_image_less_request_before_any_network_call() {
let state = ArtcraftGrokImagineVideo1p5RequestState {
request: OmniGenVideoCostAndGenerateRequest {
idempotency_token: Some("a1b2c3d4-e5f6-7890-abcd-ef1234567890".to_string()),
model: Some(CommonVideoModelEnum::GrokImagineVideo1p5),
prompt: Some("a corgi running through a field".to_string()),
negative_prompt: None,
start_frame_image_media_token: None,
end_frame_image_media_token: None,
reference_image_media_tokens: None,
reference_video_media_tokens: None,
reference_audio_media_tokens: None,
reference_character_tokens: None,
resolution: None,
aspect_ratio: None,
bitrate: None,
quality: None,
duration_seconds: Some(5),
video_batch_count: Some(1),
generate_audio: None,
},
};
// The guard fires before the client is used, so an unroutable localhost
// client with empty credentials proves no network call happens.
let client = RouterArtcraftClient::new(
ApiHost::Localhost { port: 1 },
StorytellerCredentialSet::empty(),
);
let err = state.send(&client).await.expect_err("image-less send should be rejected");
match err {
ArtcraftRouterError::Client(ClientError::ModelDoesNotSupportOption { field, .. }) => {
assert_eq!(field, "image_inputs");
}
other => panic!("expected Client(ModelDoesNotSupportOption), got {:?}", other),
}
}
}
@@ -5,11 +5,10 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_audio_cost_and_generate_request::OmniGenAudioCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::cost_response::omni_gen_audio_cost_response::OmniGenAudioCostResponse;
use artcraft_router::api::router_provider::RouterProvider;
use log::warn;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
use crate::http_server::endpoints::omni_gen::generate::audio::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::shared_utils::audio::validate_audio_request::validate_audio_request;
use crate::state::server_state::ServerState;
/// Estimate the cost of an audio generation.
@@ -29,22 +28,18 @@ pub async fn omni_gen_audio_cost_handler(
request: Json<OmniGenAudioCostAndGenerateRequest>,
_server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<OmniGenAudioCostResponse>, CommonWebError> {
validate_audio_request(&request)?;
// NB: Deliberately no input validation here. The UI polls this endpoint
// while the user is still composing the request (no prompt typed, nothing
// attached), and pricing is a total function of the model and options.
// Bad requests are rejected by the generate endpoint.
let mut builder = hydrate_to_router_request(&request)?;
builder.provider = RouterProvider::Artcraft;
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
let estimate = builder.build2()
.map_err(|e| {
warn!("Failed to build audio cost estimate: {}", e);
CommonWebError::from_error(e)
})?
.map_err(map_router_cost_error)?
.estimate_cost()
.map_err(|e| {
warn!("Failed to estimate audio cost: {}", e);
CommonWebError::from_error(e)
})?;
.map_err(map_router_cost_error)?;
Ok(Json(OmniGenAudioCostResponse {
success: true,
@@ -8,9 +8,9 @@ use artcraft_router::api::image_list_ref::ImageListRef;
use artcraft_router::api::router_provider::RouterProvider;
use artcraft_router::generate::generate_image::generate_image_request_builder::GenerateImageRequestBuilder;
use artcraft_router::generate::generate_image::image_generation_cost_estimate::ImageGenerationCostEstimate;
use log::warn;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
use crate::http_server::endpoints::omni_gen::generate::image::hydrate_to_router_request::hydrate_to_router_request;
use crate::state::server_state::ServerState;
@@ -33,18 +33,12 @@ pub async fn omni_gen_image_cost_handler(
) -> Result<Json<OmniGenImageCostResponse>, CommonWebError> {
let mut builder = hydrate_to_router_request(&request)?;
builder.provider = RouterProvider::Artcraft; // NB: Explicitly spell this out.
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
let estimate = builder.build2()
.map_err(|e| {
warn!("Failed to build cost estimate: {}", e);
CommonWebError::from_error(e)
})?
.map_err(map_router_cost_error)?
.estimate_cost()
.map_err(|e| {
warn!("Failed to estimate cost: {}", e);
CommonWebError::from_error(e)
})?;
.map_err(map_router_cost_error)?;
Ok(Json(OmniGenImageCostResponse {
success: true,
@@ -5,11 +5,10 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_mesh_cost_and_generate_request::OmniGenMeshCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::cost_response::omni_gen_mesh_cost_response::OmniGenMeshCostResponse;
use artcraft_router::api::router_provider::RouterProvider;
use log::warn;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
use crate::http_server::endpoints::omni_gen::generate::mesh::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::shared_utils::mesh::validate_mesh_request::validate_mesh_request;
use crate::state::server_state::ServerState;
/// Estimate the cost of a mesh (3D object) generation.
@@ -29,22 +28,18 @@ pub async fn omni_gen_mesh_cost_handler(
request: Json<OmniGenMeshCostAndGenerateRequest>,
_server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<OmniGenMeshCostResponse>, CommonWebError> {
validate_mesh_request(&request)?;
// NB: Deliberately no input validation here. The UI polls this endpoint
// while the user is still composing the request (no prompt typed, nothing
// attached), and pricing is a total function of the model and options.
// Bad requests are rejected by the generate endpoint.
let mut builder = hydrate_to_router_request(&request)?;
builder.provider = RouterProvider::Artcraft;
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
let estimate = builder.build2()
.map_err(|e| {
warn!("Failed to build mesh cost estimate: {}", e);
CommonWebError::from_error(e)
})?
.map_err(map_router_cost_error)?
.estimate_cost()
.map_err(|e| {
warn!("Failed to estimate mesh cost: {}", e);
CommonWebError::from_error(e)
})?;
.map_err(map_router_cost_error)?;
Ok(Json(OmniGenMeshCostResponse {
success: true,
@@ -5,11 +5,10 @@ use actix_web::{web, HttpRequest};
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_splat_cost_and_generate_request::OmniGenSplatCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::cost_response::omni_gen_splat_cost_response::OmniGenSplatCostResponse;
use artcraft_router::api::router_provider::RouterProvider;
use log::warn;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
use crate::http_server::endpoints::omni_gen::generate::splat::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::shared_utils::splat::validate_splat_request::validate_splat_request;
use crate::state::server_state::ServerState;
/// Estimate the cost of a splat (3D world) generation.
@@ -29,22 +28,18 @@ pub async fn omni_gen_splat_cost_handler(
request: Json<OmniGenSplatCostAndGenerateRequest>,
_server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<OmniGenSplatCostResponse>, CommonWebError> {
validate_splat_request(&request)?;
// NB: Deliberately no input validation here. The UI polls this endpoint
// while the user is still composing the request (no prompt typed, nothing
// attached), and pricing is a total function of the model and options.
// Bad requests are rejected by the generate endpoint.
let mut builder = hydrate_to_router_request(&request)?;
builder.provider = RouterProvider::Artcraft;
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
let estimate = builder.build2()
.map_err(|e| {
warn!("Failed to build splat cost estimate: {}", e);
CommonWebError::from_error(e)
})?
.map_err(map_router_cost_error)?
.estimate_cost()
.map_err(|e| {
warn!("Failed to estimate splat cost: {}", e);
CommonWebError::from_error(e)
})?;
.map_err(map_router_cost_error)?;
Ok(Json(OmniGenSplatCostResponse {
success: true,
@@ -1,15 +1,11 @@
use std::sync::Arc;
use crate::http_server::common_responses::common_web_error::CommonWebError;
use crate::http_server::endpoints::omni_gen::shared_utils::map_router_cost_error::map_router_cost_error;
use crate::http_server::endpoints::omni_gen::generate::video::helpers::hydrate_router_request::hydrate_to_router_request;
use crate::http_server::endpoints::omni_gen::shared_utils::video::validate_video_request::validate_video_request;
use crate::state::server_state::ServerState;
use actix_web::web::Json;
use actix_web::{web, HttpRequest};
use actix_web::HttpRequest;
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_video_cost_and_generate_request::OmniGenVideoCostAndGenerateRequest;
use artcraft_api_defs::omni_gen::cost_response::omni_gen_video_cost_response::OmniGenVideoCostResponse;
use artcraft_router::api::router_provider::RouterProvider;
use log::warn;
/// Estimate the cost of a video generation.
#[utoipa::path(
@@ -26,24 +22,19 @@ use log::warn;
pub async fn omni_gen_video_cost_handler(
_http_request: HttpRequest,
request: Json<OmniGenVideoCostAndGenerateRequest>,
_server_state: web::Data<Arc<ServerState>>,
) -> Result<Json<OmniGenVideoCostResponse>, CommonWebError> {
validate_video_request(&request)?;
// NB: Deliberately no input validation here. The UI polls this endpoint
// while the user is still composing the request (no prompt typed, nothing
// attached), and pricing is a total function of the model and options.
// Bad requests are rejected by the generate endpoint.
let mut builder = hydrate_to_router_request(&request)?;
builder.provider = RouterProvider::Artcraft;
builder.provider = RouterProvider::Artcraft; // NB: User is paying for ArtCraft credits / generation
let estimate = builder.build2()
.map_err(|e| {
warn!("Failed to build cost estimate: {}", e);
CommonWebError::from_error(e)
})?
.map_err(map_router_cost_error)?
.estimate_cost()
.map_err(|e| {
warn!("Failed to estimate cost: {}", e);
CommonWebError::from_error(e)
})?;
.map_err(map_router_cost_error)?;
Ok(Json(OmniGenVideoCostResponse {
success: true,
@@ -56,3 +47,96 @@ pub async fn omni_gen_video_cost_handler(
failures_are_refunded: estimate.failures_are_refunded,
}))
}
#[cfg(test)]
mod tests {
use actix_http::StatusCode;
use actix_web::test::TestRequest;
use actix_web::ResponseError;
use enums::common::generation::common_video_model::CommonVideoModel;
use super::*;
mod cost_without_inputs_tests {
use super::*;
/// Regression: xAI's v1.5 rejects text-to-video at generation time, but
/// the cost endpoint must still quote an image-less request — the cost
/// UI polls for a price before the user attaches an image. This request
/// shape used to be rejected (handler validation, then a router build
/// error that surfaced as a 500).
#[tokio::test]
async fn grok_1p5_without_an_image_gets_a_quote() {
let response = post_cost_request(base_request(CommonVideoModel::GrokImagineVideo1p5))
.await
.expect("image-less grok 1.5 cost estimate should succeed");
assert!(response.success);
assert!(response.cost_in_credits.unwrap() > 0);
}
#[tokio::test]
async fn bare_model_gets_a_quote_for_representative_models() {
for model in [
CommonVideoModel::GrokImagineVideo,
CommonVideoModel::Kling3p0Pro,
CommonVideoModel::Seedance2p0,
CommonVideoModel::Sora2,
CommonVideoModel::Veo3p1,
CommonVideoModel::ViduQ3,
] {
let response = post_cost_request(base_request(model))
.await
.unwrap_or_else(|e| panic!("bare-model cost estimate should succeed for {model:?}: {e:?}"));
assert!(response.cost_in_credits.unwrap() > 0, "no cost for {model:?}");
}
}
}
mod error_mapping_tests {
use super::*;
#[tokio::test]
async fn unroutable_model_returns_400_not_500() {
// grok_video has no Artcraft route in the router; this must surface
// as a 400, not a 500 (it 500'd in production before the
// map_router_cost_error mapping).
let err = post_cost_request(base_request(CommonVideoModel::GrokVideo))
.await
.expect_err("unroutable model should be rejected");
assert_eq!(err.status_code(), StatusCode::BAD_REQUEST);
}
}
async fn post_cost_request(
body: OmniGenVideoCostAndGenerateRequest,
) -> Result<OmniGenVideoCostResponse, CommonWebError> {
let http_request = TestRequest::post()
.uri("/v1/omni_gen/cost/video")
.to_http_request();
omni_gen_video_cost_handler(http_request, Json(body))
.await
.map(Json::into_inner)
}
fn base_request(model: CommonVideoModel) -> OmniGenVideoCostAndGenerateRequest {
OmniGenVideoCostAndGenerateRequest {
idempotency_token: None,
model: Some(model),
prompt: None,
negative_prompt: None,
start_frame_image_media_token: None,
end_frame_image_media_token: None,
reference_image_media_tokens: None,
reference_video_media_tokens: None,
reference_audio_media_tokens: None,
reference_character_tokens: None,
resolution: None,
aspect_ratio: None,
bitrate: None,
quality: None,
duration_seconds: None,
video_batch_count: None,
generate_audio: None,
}
}
}
@@ -7,7 +7,11 @@ const SEED_AUDIO_SAMPLE_RATES_HZ: [u32; 6] = [8000, 16000, 24000, 32000, 44100,
const SEED_AUDIO_MAX_AUDIO_REFERENCES: usize = 3;
const SEED_AUDIO_MAX_IMAGE_REFERENCES: usize = 1;
/// Validate requests before they incur user costs or send API requests.
/// Validate generation requests before they incur user costs or send API
/// requests.
///
/// NB: The cost endpoint deliberately does NOT call this — the UI polls for
/// a price while the user is still composing the request.
pub fn validate_audio_request(
request: &OmniGenAudioCostAndGenerateRequest,
) -> Result<(), CommonWebError> {
@@ -0,0 +1,29 @@
use artcraft_router::errors::artcraft_router_error::ArtcraftRouterError;
use log::warn;
use crate::http_server::common_responses::common_web_error::CommonWebError;
/// Map a router error from a cost-estimation path to an HTTP error.
///
/// Caller mistakes — a model the router can't price (e.g. no Artcraft
/// route), bad option values — surface as 400s with the router's message.
/// Infrastructure-class errors stay 500s.
pub fn map_router_cost_error(error: ArtcraftRouterError) -> CommonWebError {
match &error {
ArtcraftRouterError::InvalidInput(reason) => {
warn!("Cost estimate rejected: {}", reason);
CommonWebError::BadInputWithSimpleMessage(reason.to_string())
}
ArtcraftRouterError::Client(_)
| ArtcraftRouterError::UnsupportedModel(_)
| ArtcraftRouterError::UnsupportedProviderAndModelForNewApi(_) => {
// NB: Don't leak configuration details.
warn!("Cost estimate rejected: {}", error);
CommonWebError::BadInputWithSimpleMessage("Bad input for model".to_string())
}
_ => {
warn!("Cost estimate failed: {}", error);
CommonWebError::from_error(error)
}
}
}
@@ -18,7 +18,11 @@ const MESHY_MAX_FACE_COUNT: u64 = 300_000;
/// Rodin v2.5 Fast accepts up to five input images.
const RODIN_MAX_REFERENCE_IMAGES: usize = 5;
/// Validate requests before they incur user costs or send API requests.
/// Validate generation requests before they incur user costs or send API
/// requests.
///
/// NB: The cost endpoint deliberately does NOT call this — the UI polls for
/// a price while the user is still composing the request.
pub fn validate_mesh_request(
request: &OmniGenMeshCostAndGenerateRequest,
) -> Result<(), CommonWebError> {
@@ -1,4 +1,5 @@
pub mod audio;
pub mod map_router_cost_error;
pub mod map_seedance2pro_router_error;
pub mod mesh;
pub mod splat;
@@ -5,7 +5,11 @@ use crate::http_server::common_responses::common_web_error::CommonWebError;
/// World Labs MultiImage input accepts up to this many reference images.
const MAX_IMAGE_REFERENCES: usize = 4;
/// Validate requests before they incur user costs or send API requests.
/// Validate generation requests before they incur user costs or send API
/// requests.
///
/// NB: The cost endpoint deliberately does NOT call this — the UI polls for
/// a price while the user is still composing the request.
///
/// Model presence is enforced later during hydration; the input-shape rules
/// below apply uniformly to all Marble models.