artcraft desktop gpt-image cost estimates + quality arg (#1425)

This commit is contained in:
Brandon Thomas
2026-04-22 19:17:07 -04:00
committed by GitHub
parent 31a9704faf
commit 5eb2abf112
15 changed files with 87 additions and 18 deletions
@@ -15,11 +15,12 @@ body:
type: json
content: |
{
"model": "seedream_5_lite",
"model": "gpt_image_2",
"provider": "artcraft",
"generation_mode": {"type": "text_to_image" },
"aspect_ratio": "wide_sixteen_by_nine",
"resolution": "two_k",
"quality": "low",
"image_batch_count": 1
}
order: 2000
@@ -3,13 +3,14 @@ use utoipa::ToSchema;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_image_model::CommonImageModel;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation_provider::GenerationProvider;
pub const ESTIMATE_IMAGE_COST_PATH: &str = "/v1/generate/cost_estimate/image";
/// Request body for the image cost estimate endpoint.
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
pub struct EstimateImageCostRequest {
/// The image model to estimate costs for.
pub model: CommonImageModel,
@@ -29,12 +30,15 @@ pub struct EstimateImageCostRequest {
/// Optional resolution.
pub resolution: Option<CommonResolution>,
// Optional quality.
pub quality: Option<CommonQuality>,
/// Number of images to generate in parallel.
pub image_batch_count: Option<u16>,
}
/// Describes the type of image generation being requested.
#[derive(Serialize, Deserialize, ToSchema)]
#[derive(Serialize, Deserialize, ToSchema, Debug)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum GenerationMode {
/// No input images — generate from prompt only.
@@ -94,6 +98,7 @@ mod tests {
generation_mode: GenerationMode::TextToImage,
aspect_ratio: None,
resolution: None,
quality: None,
image_batch_count: None,
};
let serialized = serde_json::to_string(&request).unwrap();
@@ -108,6 +113,7 @@ mod tests {
generation_mode: GenerationMode::ImageEdit { count: 2 },
aspect_ratio: None,
resolution: None,
quality: None,
image_batch_count: None,
};
let serialized = serde_json::to_string(&request).unwrap();
@@ -17,7 +17,7 @@ pub async fn estimate_image_cost_command(
request: EstimateImageCostRequest,
app_env_configs: State<'_, AppEnvConfigs>,
) -> ResponseOrError<EstimateImageCostResponse, EstimateImageCostError> {
debug!("estimate_image_cost_command called");
debug!("estimate_image_cost_command called: {:?}", request);
let result = estimate_image_cost(
&app_env_configs.storyteller_host,
@@ -378,6 +378,10 @@ fn build_omni_gen_image_model_providers() -> Vec<OmniGenImageModelProviderDetail
model: CommonImageModel::GptImage1p5,
overrides: None,
},
OmniGenImageProviderModelDetails {
model: CommonImageModel::GptImage2,
overrides: None,
},
OmniGenImageProviderModelDetails {
model: CommonImageModel::Seedream4,
overrides: None,
@@ -9,6 +9,7 @@ use artcraft_api_defs::generate::cost_estimate::estimate_image_cost::{
};
use artcraft_router::api::common_aspect_ratio::CommonAspectRatio as RouterAspectRatio;
use artcraft_router::api::common_image_model::CommonImageModel as RouterImageModel;
use artcraft_router::api::common_quality::CommonQuality as RouterQuality;
use artcraft_router::api::common_resolution::CommonResolution as RouterResolution;
use artcraft_router::api::provider::Provider as RouterProvider;
use artcraft_router::client::generation_mode_mismatch_strategy::GenerationModeMismatchStrategy;
@@ -16,6 +17,7 @@ use artcraft_router::client::request_mismatch_mitigation_strategy::RequestMismat
use artcraft_router::generate::generate_image::generate_image_request::GenerateImageRequest;
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
use enums::common::generation::common_image_model::CommonImageModel;
use enums::common::generation::common_quality::CommonQuality;
use enums::common::generation::common_resolution::CommonResolution;
use enums::common::generation_provider::GenerationProvider;
@@ -37,6 +39,7 @@ pub async fn estimate_image_cost_handler(
let router_model = map_image_model(request.model)?;
let router_aspect_ratio = request.aspect_ratio.map(map_aspect_ratio);
let router_resolution = request.resolution.map(map_resolution);
let router_quality = request.quality.map(map_quality);
let router_request = GenerateImageRequest {
model: router_model,
@@ -45,7 +48,7 @@ pub async fn estimate_image_cost_handler(
image_inputs: None, // TODO: Only some models charge for this - we'll need to add later.
resolution: router_resolution,
aspect_ratio: router_aspect_ratio,
quality: None,
quality: router_quality,
image_batch_count: request.image_batch_count,
request_mismatch_mitigation_strategy: RequestMismatchMitigationStrategy::PayLessDowngrade,
generation_mode_mismatch_strategy: Some(GenerationModeMismatchStrategy::GenerateAnyway),
@@ -132,13 +135,13 @@ fn map_image_model(model: CommonImageModel) -> Result<RouterImageModel, HandlerE
CommonImageModel::FluxPro11Ultra => RouterImageModel::FluxPro11Ultra,
CommonImageModel::GptImage1 => RouterImageModel::GptImage1,
CommonImageModel::GptImage1p5 => RouterImageModel::GptImage1p5,
CommonImageModel::GptImage2 => RouterImageModel::GptImage2,
CommonImageModel::NanoBanana => RouterImageModel::NanoBanana,
CommonImageModel::NanoBanana2 => RouterImageModel::NanoBanana2,
CommonImageModel::NanoBananaPro => RouterImageModel::NanoBananaPro,
CommonImageModel::Seedream4 => RouterImageModel::Seedream4,
CommonImageModel::Seedream4p5 => RouterImageModel::Seedream4p5,
CommonImageModel::Seedream5Lite => RouterImageModel::Seedream5Lite,
CommonImageModel::GptImage2 => RouterImageModel::GptImage2,
};
Ok(router_model)
}
@@ -178,3 +181,11 @@ fn map_resolution(res: CommonResolution) -> RouterResolution {
CommonResolution::TenEightyP => RouterResolution::TenEightyP,
}
}
fn map_quality(res: CommonQuality) -> RouterQuality {
match res {
CommonQuality::Low => RouterQuality::Low,
CommonQuality::Medium => RouterQuality::Medium,
CommonQuality::High => RouterQuality::High,
}
}
@@ -9,6 +9,7 @@ export enum CommonImageModel {
FluxPro11Ultra = "flux_pro_1p1_ultra",
GptImage1 = "gpt_image_1",
GptImage1p5 = "gpt_image_1p5",
GptImage2 = "gpt_image_2",
NanaBanana = "nano_banana",
NanaBanana2 = "nano_banana_2",
NanaBananaPro = "nano_banana_pro",
@@ -0,0 +1,9 @@
// NOTE: These are defined in Rust (as the source of truth) and duplicated in the frontend.
// In the future, we should use code gen (protobufs or similar) to keep the two sides in sync.
export enum CommonQuality {
High = "high",
Medium = "medium",
Low = "low",
}
@@ -1,6 +1,7 @@
export * from "./CommonImageModel.js";
export * from "./CommonVideoModel.js";
export * from "./CommonAspectRatio.js";
export * from "./CommonQuality.js";
export * from "./CommonVideoResolution.js";
export * from "./GenerationMode.js";
export * from "./ImageGenerationMode.js";
@@ -14,8 +14,12 @@ export function imageModelToCommonImageModel(
return CommonImageModel.FluxPro11;
case "flux_pro_11_ultra":
return CommonImageModel.FluxPro11Ultra;
case "gpt_image_1":
return CommonImageModel.GptImage1;
case "gpt_image_1p5":
return CommonImageModel.GptImage1p5;
case "gpt_image_2":
return CommonImageModel.GptImage2;
case "nano_banana":
return CommonImageModel.NanaBanana;
case "nano_banana_2":
@@ -0,0 +1,21 @@
import { CommonQuality } from "@storyteller/api-enums";
/**
* Convert a quality value to CommonQuality for the image cost estimate API.
*
* Accepts a CommonQuality string value from @storyteller/model-list ("high", "medium", "low").
*/
export function imageQualityToCommonQuality(
quality: string | undefined,
): CommonQuality | null {
switch (quality) {
case "high":
return CommonQuality.High;
case "medium":
return CommonQuality.Medium;
case "low":
return CommonQuality.Low;
default:
return null;
}
}
@@ -3,5 +3,6 @@ export * from "./videoAspectRatioToCommonAspectRatio.js";
export * from "./videoStoreToGenerationMode.js";
export * from "./imageModelToCommonImageModel.js";
export * from "./imageAspectRatioToCommonAspectRatio.js";
export * from "./imageQualityToCommonQuality.js";
export * from "./imageResolutionToCommonVideoResolution.js";
export * from "./imageStoreToGenerationMode.js";
@@ -277,6 +277,7 @@ export function CostBreakdownModal({ activeTabId }: CostBreakdownModalProps) {
"flux_pro_11",
"flux_pro_11_ultra",
"gpt_image_1p5",
"gpt_image_2",
"nano_banana",
"nano_banana_2",
"nano_banana_pro",
@@ -16,6 +16,7 @@ import { useCostBreakdownModalStore } from "./cost-breakdown-modal-store";
import {
imageModelToCommonImageModel,
imageAspectRatioToCommonAspectRatio,
imageQualityToCommonQuality,
imageResolutionToCommonVideoResolution,
} from "./convert/index.js";
@@ -44,6 +45,7 @@ export function useImageCostEstimate(
const imageLegacyResolution = usePromptImageStore((s) => s.resolution);
const imageReferenceImages = usePromptImageStore((s) => s.referenceImages);
const imageGenerationCount = usePromptImageStore((s) => s.generationCount);
const imageQuality = usePromptImageStore((s) => s.commonQuality);
// Canvas2D store
const prompt2DAspectRatio = usePrompt2DStore((s) => s.aspectRatio);
@@ -74,6 +76,7 @@ export function useImageCostEstimate(
let aspectRatioStr: string | undefined;
let legacyAspectRatioStr: string | undefined;
let resolutionStr: string | undefined;
let qualityStr: string | undefined;
let referenceImageCount = 0;
let generationCount = 1;
@@ -82,6 +85,7 @@ export function useImageCostEstimate(
aspectRatioStr = imageAspectRatio;
legacyAspectRatioStr = imageLegacyAspectRatio;
resolutionStr = imageResolution ?? imageLegacyResolution;
qualityStr = imageQuality;
referenceImageCount = imageReferenceImages.length;
generationCount = imageGenerationCount;
break;
@@ -114,6 +118,7 @@ export function useImageCostEstimate(
);
const commonResolution =
imageResolutionToCommonVideoResolution(resolutionStr);
const commonQuality = imageQualityToCommonQuality(qualityStr);
const generationMode =
referenceImageCount > 0
? { type: "image_edit" as const, count: referenceImageCount }
@@ -131,6 +136,7 @@ export function useImageCostEstimate(
generation_mode: generationMode,
aspect_ratio: commonAspectRatio ?? undefined,
resolution: commonResolution ?? undefined,
quality: commonQuality ?? undefined,
})
.then((result) => {
if (isEstimateImageCostSuccess(result)) {
@@ -160,6 +166,7 @@ export function useImageCostEstimate(
imageLegacyResolution,
imageReferenceImages.length,
imageGenerationCount,
imageQuality,
prompt2DAspectRatio,
prompt2DResolution,
prompt2DReferenceImages.length,
@@ -296,12 +296,12 @@ export const IMAGE_MODELS: ImageModel[] = [
progressBarTime: 60000,
maxPromptLength: 1500,
canChangeAspectRatio: true,
// qualityOptions: [
// CommonQuality.High,
// CommonQuality.Medium,
// CommonQuality.Low,
// ],
// defaultQuality: CommonQuality.High,
defaultQuality: CommonQuality.High,
qualityOptions: [
CommonQuality.High,
CommonQuality.Medium,
CommonQuality.Low,
],
}),
new ImageModel({
id: "gpt_image_1p5",
@@ -327,12 +327,12 @@ export const IMAGE_MODELS: ImageModel[] = [
CommonAspectRatio.Wide,
CommonAspectRatio.Tall,
],
// qualityOptions: [
// CommonQuality.High,
// CommonQuality.Medium,
// CommonQuality.Low,
// ],
// defaultQuality: CommonQuality.High,
defaultQuality: CommonQuality.High,
qualityOptions: [
CommonQuality.High,
CommonQuality.Medium,
CommonQuality.Low,
],
}),
new ImageModel({
id: "gpt_image_2",
@@ -3,6 +3,7 @@ import { CommandResult, CommandSuccessStatus } from "../common/CommandStatus";
import {
CommonImageModel,
CommonAspectRatio,
CommonQuality,
CommonVideoResolution,
ImageGenerationMode,
GenerationProvider,
@@ -14,6 +15,7 @@ export interface EstimateImageCostRequest {
generation_mode: ImageGenerationMode;
aspect_ratio?: CommonAspectRatio;
resolution?: CommonVideoResolution;
quality?: CommonQuality;
image_batch_count?: number;
}