Follow-up fixes for the #3775 audit findings: - Bill Grok video generation per second of output, matching the xAI rate card: parse the request duration (1-15s, upstream default 8s) and compute cost as per-second price x duration x count. The built-in rate card values were already xAI per-second prices but were previously charged per video, undercharging up to 15x with a user-controlled duration. - Group video_price_* fields are now documented and surfaced as per-second rates (USD/s); admin UI labels, placeholders and hints updated accordingly. - Persist video_count/video_resolution/video_duration_seconds on usage_logs (migration 172) so video billing is auditable, and exempt any row with video_count > 0 from the image_size check constraint: a video billed via a token-mode channel price produces billing_mode='token' with image_count=1 and no image_size, which the previous constraint rejected, dropping the whole billing transaction. - Only refetch the group in apiKeyWithFreshGroupMediaPricing when the group object actually looks like it is missing media pricing fields (both media multipliers zero and all prices nil, impossible for a normally loaded group), removing a per-usage DB query for groups without overrides. - Frontend: drop the unused admin.groups.mediaPricing locale block, map cleared price inputs to null (create) / -1 (update, cleared via backend normalizePrice) instead of sending "" that failed *float64 unmarshalling, and align video price placeholders with the text-to-video default model (grok-imagine-video 0.05/0.07, 1080p only on 1.5 at 0.25).
94 lines
2.4 KiB
TypeScript
94 lines
2.4 KiB
TypeScript
export const imagePricingPlatforms = new Set([
|
|
"antigravity",
|
|
"gemini",
|
|
"grok",
|
|
"openai",
|
|
]);
|
|
|
|
export const supportsImagePricingPlatform = (platform: string): boolean =>
|
|
imagePricingPlatforms.has(platform);
|
|
|
|
export const supportsVideoPricingPlatform = (platform: string): boolean =>
|
|
platform === "grok";
|
|
|
|
export const imagePricingI18nKey = (_platform: string, key: string): string =>
|
|
`admin.groups.imagePricing.${key}`;
|
|
|
|
export const videoPricingI18nKey = (key: string): string =>
|
|
`admin.groups.videoPricing.${key}`;
|
|
|
|
type ImagePricingTierKey = "image_price_1k" | "image_price_2k" | "image_price_4k";
|
|
type VideoPricingTierKey =
|
|
| "video_price_480p"
|
|
| "video_price_720p"
|
|
| "video_price_1080p";
|
|
|
|
const defaultImagePricePlaceholders: Record<
|
|
string,
|
|
Record<ImagePricingTierKey, string>
|
|
> = {
|
|
default: {
|
|
image_price_1k: "0.134",
|
|
image_price_2k: "0.201",
|
|
image_price_4k: "0.268",
|
|
},
|
|
grok: {
|
|
image_price_1k: "0.02",
|
|
image_price_2k: "0.02",
|
|
image_price_4k: "0.02",
|
|
},
|
|
};
|
|
|
|
// 视频价为每秒单价(USD/s)。480p/720p 取 grok-imagine-video(文生视频实际走该模型)的
|
|
// 官方每秒价;1080p 仅 grok-imagine-video-1.5 图生视频支持,取 1.5 的每秒价。
|
|
const defaultVideoPricePlaceholders: Record<
|
|
string,
|
|
Record<VideoPricingTierKey, string>
|
|
> = {
|
|
grok: {
|
|
video_price_480p: "0.05",
|
|
video_price_720p: "0.07",
|
|
video_price_1080p: "0.25",
|
|
},
|
|
};
|
|
|
|
export const getImagePricePlaceholder = (
|
|
platform: string,
|
|
tier: ImagePricingTierKey,
|
|
): string => {
|
|
const card = defaultImagePricePlaceholders[platform] ?? defaultImagePricePlaceholders.default;
|
|
return card[tier];
|
|
};
|
|
|
|
export const getVideoPricePlaceholder = (
|
|
platform: string,
|
|
tier: VideoPricingTierKey,
|
|
): string => {
|
|
const card = defaultVideoPricePlaceholders[platform];
|
|
return card?.[tier] ?? "";
|
|
};
|
|
|
|
export const getDefaultImagePreviewPrice = (
|
|
platform: string,
|
|
tier: ImagePricingTierKey,
|
|
): number | null => {
|
|
const placeholder = getImagePricePlaceholder(platform, tier);
|
|
if (placeholder === "") {
|
|
return null;
|
|
}
|
|
const value = Number(placeholder);
|
|
return Number.isFinite(value) ? value : null;
|
|
};
|
|
|
|
export const getDefaultVideoPreviewPrice = (
|
|
platform: string,
|
|
tier: VideoPricingTierKey,
|
|
): number | null => {
|
|
const placeholder = getVideoPricePlaceholder(platform, tier);
|
|
if (placeholder === "") {
|
|
return null;
|
|
}
|
|
const value = Number(placeholder);
|
|
return Number.isFinite(value) ? value : null;
|
|
};
|