mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-31 08:25:35 +00:00
fix: bill Grok video per second and harden video usage logging
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).
This commit is contained in:
@@ -841,26 +841,15 @@ export default {
|
||||
finalPricePreview: 'Final per-image price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
mediaPricing: {
|
||||
title: 'Image / Video Generation Pricing',
|
||||
description:
|
||||
'Configure Grok image and video generation access plus base media prices. Leave empty to use default prices.',
|
||||
allowImageGeneration: 'Allow image and video generation for this group',
|
||||
independentMultiplier: 'Use independent media multiplier',
|
||||
imageMultiplier: 'Media multiplier',
|
||||
modeHint:
|
||||
'By default, Grok media billing uses media price × current effective group multiplier. Independent mode uses media price × media multiplier. One video generation is billed as one media unit.',
|
||||
finalPricePreview: 'Final per-media-unit price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
videoPricing: {
|
||||
title: 'Video Generation Pricing',
|
||||
description: 'Configure Grok video generation base prices. Leave empty to use default video prices.',
|
||||
description:
|
||||
'Configure Grok video generation prices in USD per second of output video. Leave empty to use the default per-second rates (grok-imagine-video: $0.05/s 480p, $0.07/s 720p; video-1.5: $0.08/s 480p, $0.14/s 720p, $0.25/s 1080p).',
|
||||
independentMultiplier: 'Use independent video multiplier',
|
||||
videoMultiplier: 'Video multiplier',
|
||||
modeHint:
|
||||
'By default, video billing uses video price × current effective group multiplier. Independent mode uses video price × video multiplier.',
|
||||
finalPricePreview: 'Final per-video price preview',
|
||||
'Videos are billed per second: per-second price × duration (1-15s, default 8s). By default the current effective group multiplier applies; independent mode uses the video multiplier instead.',
|
||||
finalPricePreview: 'Final per-second price preview',
|
||||
notConfigured: 'Not configured'
|
||||
},
|
||||
peakRate: {
|
||||
|
||||
@@ -919,25 +919,15 @@ export default {
|
||||
finalPricePreview: '最终单张价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
mediaPricing: {
|
||||
title: '图片/视频生成计费',
|
||||
description: '配置 Grok 图片和视频生成能力及媒体基础单价,留空则使用默认价格',
|
||||
allowImageGeneration: '允许当前分组生图和视频生成',
|
||||
independentMultiplier: '媒体倍率独立',
|
||||
imageMultiplier: '媒体独立倍率',
|
||||
modeHint:
|
||||
'默认关闭独立倍率时,Grok 媒体费用 = 媒体价格 × 当前分组有效倍率;开启独立倍率后,Grok 媒体费用 = 媒体价格 × 媒体独立倍率。一次视频生成按 1 个媒体单位计费。',
|
||||
finalPricePreview: '最终单次媒体价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
videoPricing: {
|
||||
title: '视频生成计费',
|
||||
description: '配置 Grok 视频生成基础单价,留空则使用默认视频价格',
|
||||
description:
|
||||
'配置 Grok 视频生成的每秒单价(USD/秒),留空则使用默认每秒价(grok-imagine-video:480p $0.05/s、720p $0.07/s;video-1.5:480p $0.08/s、720p $0.14/s、1080p $0.25/s)',
|
||||
independentMultiplier: '视频倍率独立',
|
||||
videoMultiplier: '视频独立倍率',
|
||||
modeHint:
|
||||
'默认关闭独立倍率时,视频费用 = 视频价格 × 当前分组有效倍率;开启独立倍率后,视频费用 = 视频价格 × 视频独立倍率。',
|
||||
finalPricePreview: '最终单次视频价格预览',
|
||||
'视频按秒计费:费用 = 每秒价格 × 时长(1-15 秒,未指定默认 8 秒)。默认叠加当前分组有效倍率;开启独立倍率后改用视频独立倍率。',
|
||||
finalPricePreview: '最终每秒价格预览',
|
||||
notConfigured: '未配置'
|
||||
},
|
||||
peakRate: {
|
||||
|
||||
@@ -1010,7 +1010,7 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">480p ($)</label>
|
||||
<label class="input-label">480p ($/s)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_480p"
|
||||
type="number"
|
||||
@@ -1021,7 +1021,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">720p ($)</label>
|
||||
<label class="input-label">720p ($/s)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_720p"
|
||||
type="number"
|
||||
@@ -1032,7 +1032,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">1080p ($)</label>
|
||||
<label class="input-label">1080p ($/s)</label>
|
||||
<input
|
||||
v-model.number="createForm.video_price_1080p"
|
||||
type="number"
|
||||
@@ -2489,7 +2489,7 @@
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label class="input-label">480p ($)</label>
|
||||
<label class="input-label">480p ($/s)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_480p"
|
||||
type="number"
|
||||
@@ -2500,7 +2500,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">720p ($)</label>
|
||||
<label class="input-label">720p ($/s)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_720p"
|
||||
type="number"
|
||||
@@ -2511,7 +2511,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="input-label">1080p ($)</label>
|
||||
<label class="input-label">1080p ($/s)</label>
|
||||
<input
|
||||
v-model.number="editForm.video_price_1080p"
|
||||
type="number"
|
||||
@@ -4718,6 +4718,14 @@ const handleCreateGroup = async () => {
|
||||
requestData.video_rate_multiplier = normalizeRateMultiplier(
|
||||
requestData.video_rate_multiplier,
|
||||
);
|
||||
// 媒体价格输入清空时 v-model.number 产生 "",直接提交会被后端 *float64 反序列化拒绝(400),
|
||||
// 创建时按"未配置"(null)处理。
|
||||
requestData.image_price_1k = emptyToNull(requestData.image_price_1k);
|
||||
requestData.image_price_2k = emptyToNull(requestData.image_price_2k);
|
||||
requestData.image_price_4k = emptyToNull(requestData.image_price_4k);
|
||||
requestData.video_price_480p = emptyToNull(requestData.video_price_480p);
|
||||
requestData.video_price_720p = emptyToNull(requestData.video_price_720p);
|
||||
requestData.video_price_1080p = emptyToNull(requestData.video_price_1080p);
|
||||
requestData.peak_rate_enabled = createForm.peak_rate_enabled;
|
||||
requestData.peak_start = createForm.peak_start;
|
||||
requestData.peak_end = createForm.peak_end;
|
||||
@@ -4896,6 +4904,16 @@ const handleUpdateGroup = async () => {
|
||||
payload.video_rate_multiplier = normalizeRateMultiplier(
|
||||
payload.video_rate_multiplier,
|
||||
);
|
||||
// 媒体价格输入清空时 v-model.number 产生 "",直接提交会被后端 *float64 反序列化拒绝(400)。
|
||||
// 更新语义中 null 表示"不修改",因此清空后的字段发送 -1:后端 normalizePrice 将负价归一为
|
||||
// NULL,从而真正清除已配置的价格。
|
||||
const emptyPriceToClear = (v: any) => (v === "" || v === null ? -1 : v);
|
||||
payload.image_price_1k = emptyPriceToClear(payload.image_price_1k);
|
||||
payload.image_price_2k = emptyPriceToClear(payload.image_price_2k);
|
||||
payload.image_price_4k = emptyPriceToClear(payload.image_price_4k);
|
||||
payload.video_price_480p = emptyPriceToClear(payload.video_price_480p);
|
||||
payload.video_price_720p = emptyPriceToClear(payload.video_price_720p);
|
||||
payload.video_price_1080p = emptyPriceToClear(payload.video_price_1080p);
|
||||
payload.peak_rate_enabled = editForm.peak_rate_enabled;
|
||||
payload.peak_start = editForm.peak_start;
|
||||
payload.peak_end = editForm.peak_end;
|
||||
|
||||
@@ -37,8 +37,10 @@ describe("groups image pricing platform support", () => {
|
||||
it("uses Grok media defaults instead of generic image fallback placeholders", () => {
|
||||
expect(getImagePricePlaceholder("grok", "image_price_1k")).toBe("0.02");
|
||||
expect(getImagePricePlaceholder("grok", "image_price_2k")).toBe("0.02");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_480p")).toBe("0.08");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_720p")).toBe("0.14");
|
||||
// 视频 placeholder 为每秒单价:480p/720p 取 grok-imagine-video 官方每秒价,
|
||||
// 1080p 仅 video-1.5 支持、取 1.5 每秒价。
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_480p")).toBe("0.05");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_720p")).toBe("0.07");
|
||||
expect(getVideoPricePlaceholder("grok", "video_price_1080p")).toBe("0.25");
|
||||
});
|
||||
|
||||
|
||||
@@ -39,13 +39,15 @@ const defaultImagePricePlaceholders: Record<
|
||||
},
|
||||
};
|
||||
|
||||
// 视频价为每秒单价(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.08",
|
||||
video_price_720p: "0.14",
|
||||
video_price_480p: "0.05",
|
||||
video_price_720p: "0.07",
|
||||
video_price_1080p: "0.25",
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user