fix: update Grok media default rate card

This commit is contained in:
Heatherm Huang
2026-07-08 13:50:49 +08:00
parent 4d702e3234
commit 376e03ded1
6 changed files with 276 additions and 18 deletions
+83 -2
View File
@@ -1235,6 +1235,21 @@ type VideoPriceConfig struct {
Price1080P *float64 // 1080p 视频价格(nil 表示使用默认值)
}
const (
defaultImageGenerationPrice = 0.134
defaultGrokImagineImagePrice1K = 0.02
defaultGrokImagineImagePrice2K = 0.02
defaultGrokImagineImageQualityPrice1K = 0.05
defaultGrokImagineImageQualityPrice2K = 0.07
defaultGrokImagineVideoPrice480P = 0.05
defaultGrokImagineVideoPrice720P = 0.07
defaultGrokImagineVideo15Price480P = 0.08
defaultGrokImagineVideo15Price720P = 0.14
defaultGrokImagineVideo15Price1080P = 0.25
)
// CalculateImageCost 计算图片生成费用
// model: 请求的模型名称(用于获取 LiteLLM 默认价格)
// imageSize: 图片尺寸 "1K", "2K", "4K"
@@ -1340,6 +1355,10 @@ func (s *BillingService) getVideoUnitPrice(model string, resolution string, grou
// getDefaultImagePrice 获取 LiteLLM 默认图片价格
func (s *BillingService) getDefaultImagePrice(model string, imageSize string) float64 {
if price, ok := getDefaultGrokImagineImagePrice(model, imageSize); ok {
return price
}
basePrice := 0.0
// 从 PricingService 获取 output_cost_per_image
@@ -1352,7 +1371,7 @@ func (s *BillingService) getDefaultImagePrice(model string, imageSize string) fl
// 如果没有找到价格,使用硬编码默认值($0.134,来自 gemini-3-pro-image-preview)
if basePrice <= 0 {
basePrice = 0.134
basePrice = defaultImageGenerationPrice
}
// 2K 尺寸 1.5 倍,4K 尺寸翻倍
@@ -1367,9 +1386,71 @@ func (s *BillingService) getDefaultImagePrice(model string, imageSize string) fl
}
func (s *BillingService) getDefaultVideoPrice(model string, resolution string) float64 {
_ = resolution
if price, ok := getDefaultGrokImagineVideoPrice(model, resolution); ok {
return price
}
// The bundled LiteLLM schema does not expose an output video generation price.
// Keep the historical model default as the fallback, while letting group-level
// video prices override it independently from image prices.
return s.getDefaultImagePrice(model, ImageBillingSize2K)
}
func getDefaultGrokImagineImagePrice(model string, imageSize string) (float64, bool) {
model = strings.ToLower(strings.TrimSpace(model))
switch model {
case "grok-imagine-image-quality":
return getGrokImagineImageTierPrice(
imageSize,
defaultGrokImagineImageQualityPrice1K,
defaultGrokImagineImageQualityPrice2K,
), true
case "grok-imagine", "grok-imagine-image", "grok-imagine-edit":
return getGrokImagineImageTierPrice(
imageSize,
defaultGrokImagineImagePrice1K,
defaultGrokImagineImagePrice2K,
), true
default:
return 0, false
}
}
func getGrokImagineImageTierPrice(imageSize string, price1K float64, price2K float64) float64 {
switch NormalizeImageBillingTierOrDefault(imageSize) {
case ImageBillingSize1K:
return price1K
case ImageBillingSize2K, ImageBillingSize4K:
return price2K
default:
return price2K
}
}
func getDefaultGrokImagineVideoPrice(model string, resolution string) (float64, bool) {
model = strings.ToLower(strings.TrimSpace(model))
switch {
case strings.HasPrefix(model, "grok-imagine-video-1.5"):
switch NormalizeVideoBillingResolutionOrDefault(resolution) {
case VideoBillingResolution480P:
return defaultGrokImagineVideo15Price480P, true
case VideoBillingResolution720P:
return defaultGrokImagineVideo15Price720P, true
case VideoBillingResolution1080P:
return defaultGrokImagineVideo15Price1080P, true
default:
return defaultGrokImagineVideo15Price480P, true
}
case strings.HasPrefix(model, "grok-imagine-video"):
switch NormalizeVideoBillingResolutionOrDefault(resolution) {
case VideoBillingResolution480P:
return defaultGrokImagineVideoPrice480P, true
case VideoBillingResolution720P, VideoBillingResolution1080P:
return defaultGrokImagineVideoPrice720P, true
default:
return defaultGrokImagineVideoPrice480P, true
}
default:
return 0, false
}
}
@@ -886,6 +886,36 @@ func TestCalculateVideoCostUsesSeparateConfig(t *testing.T) {
require.Equal(t, string(BillingModeVideo), videoCost.BillingMode)
}
func TestCalculateGrokImagineImageCostUsesDefaultRateCard(t *testing.T) {
svc := newTestBillingService()
standard1K := svc.CalculateImageCost("grok-imagine-image", "1K", 1, nil, 1.0)
standard2K := svc.CalculateImageCost("grok-imagine-image", "2K", 1, nil, 1.0)
quality1K := svc.CalculateImageCost("grok-imagine-image-quality", "1K", 1, nil, 1.0)
quality2K := svc.CalculateImageCost("grok-imagine-image-quality", "2K", 1, nil, 1.0)
require.InDelta(t, 0.02, standard1K.TotalCost, 1e-10)
require.InDelta(t, 0.02, standard2K.TotalCost, 1e-10)
require.InDelta(t, 0.05, quality1K.TotalCost, 1e-10)
require.InDelta(t, 0.07, quality2K.TotalCost, 1e-10)
}
func TestCalculateGrokImagineVideoCostUsesDefaultRateCard(t *testing.T) {
svc := newTestBillingService()
standard480P := svc.CalculateVideoCost("grok-imagine-video", "480p", 1, nil, 1.0)
standard720P := svc.CalculateVideoCost("grok-imagine-video", "720p", 1, nil, 1.0)
video15_480P := svc.CalculateVideoCost("grok-imagine-video-1.5", "480p", 1, nil, 1.0)
video15_720P := svc.CalculateVideoCost("grok-imagine-video-1.5", "720p", 1, nil, 1.0)
video15_1080P := svc.CalculateVideoCost("grok-imagine-video-1.5", "1080p", 1, nil, 1.0)
require.InDelta(t, 0.05, standard480P.TotalCost, 1e-10)
require.InDelta(t, 0.07, standard720P.TotalCost, 1e-10)
require.InDelta(t, 0.08, video15_480P.TotalCost, 1e-10)
require.InDelta(t, 0.14, video15_720P.TotalCost, 1e-10)
require.InDelta(t, 0.25, video15_1080P.TotalCost, 1e-10)
}
func TestIsModelSupported(t *testing.T) {
svc := newTestBillingService()
@@ -1853,6 +1853,45 @@ func TestGrokVideoBillingUsesSeparateVideoRateMultiplier(t *testing.T) {
require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode)
}
func TestOpenAIGatewayServiceRecordUsage_GrokVideoUsesDefaultRateCard(t *testing.T) {
groupID := int64(1261)
usageRepo := &openAIRecordUsageLogRepoStub{inserted: true}
svc := newOpenAIRecordUsageServiceForTest(usageRepo, &openAIRecordUsageUserRepoStub{}, &openAIRecordUsageSubRepoStub{}, nil)
err := svc.RecordUsage(context.Background(), &OpenAIRecordUsageInput{
Result: &OpenAIForwardResult{
RequestID: "video-default-rate-card",
ResponseID: "video-default-rate-card",
Model: "grok-imagine-video-1.5",
BillingModel: "grok-imagine-video-1.5",
ImageCount: 1,
VideoCount: 1,
VideoResolution: VideoBillingResolution720P,
Duration: time.Second,
},
APIKey: &APIKey{
ID: 101261,
GroupID: i64p(groupID),
Group: &Group{
ID: groupID,
Platform: PlatformGrok,
RateMultiplier: 1,
},
},
User: &User{ID: 201261},
Account: &Account{ID: 301261, Platform: PlatformGrok},
})
require.NoError(t, err)
require.NotNil(t, usageRepo.lastLog)
require.Nil(t, usageRepo.lastLog.ImageSize)
require.InDelta(t, 0.14, usageRepo.lastLog.TotalCost, 1e-12)
require.InDelta(t, 0.14, usageRepo.lastLog.ActualCost, 1e-12)
require.Equal(t, 1, usageRepo.lastLog.ImageCount)
require.NotNil(t, usageRepo.lastLog.BillingMode)
require.Equal(t, string(BillingModeVideo), *usageRepo.lastLog.BillingMode)
}
func TestOpenAIGatewayServiceRecordUsage_GroupImagePriceOverridesChannelImagePrice(t *testing.T) {
groupID := int64(127)
channelPrice := 0.201
+33 -16
View File
@@ -843,7 +843,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.134"
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_1k')"
/>
</div>
<div>
@@ -854,7 +854,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_2k')"
/>
</div>
<div>
@@ -865,7 +865,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.268"
:placeholder="getImagePricePlaceholder(createForm.platform, 'image_price_4k')"
/>
</div>
</div>
@@ -987,7 +987,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_480p')"
/>
</div>
<div>
@@ -998,7 +998,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_720p')"
/>
</div>
<div>
@@ -1009,7 +1009,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getVideoPricePlaceholder(createForm.platform, 'video_price_1080p')"
/>
</div>
</div>
@@ -2322,7 +2322,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.134"
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_1k')"
/>
</div>
<div>
@@ -2333,7 +2333,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_2k')"
/>
</div>
<div>
@@ -2344,7 +2344,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.268"
:placeholder="getImagePricePlaceholder(editForm.platform, 'image_price_4k')"
/>
</div>
</div>
@@ -2466,7 +2466,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_480p')"
/>
</div>
<div>
@@ -2477,7 +2477,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_720p')"
/>
</div>
<div>
@@ -2488,7 +2488,7 @@
step="0.001"
min="0"
class="input"
placeholder="0.201"
:placeholder="getVideoPricePlaceholder(editForm.platform, 'video_price_1080p')"
/>
</div>
</div>
@@ -3497,6 +3497,10 @@ import {
import { createModelsListCandidatesTracker } from "./groupsModelsListCandidates";
import { normalizeSupportedModelScopesForPlatform } from "./groupsSupportedModelScopes";
import {
getDefaultImagePreviewPrice,
getDefaultVideoPreviewPrice,
getImagePricePlaceholder,
getVideoPricePlaceholder,
imagePricingI18nKey,
supportsImagePricingPlatform,
supportsVideoPricingPlatform,
@@ -4244,6 +4248,7 @@ type ImagePricingFormState = {
};
type VideoPricingFormState = {
platform: GroupPlatform;
rate_multiplier: number;
video_rate_independent: boolean;
video_rate_multiplier: number;
@@ -4272,6 +4277,14 @@ const normalizePreviewNumber = (value: number | string | null | undefined, fallb
return Number.isFinite(parsed) ? parsed : fallback;
};
const parsePreviewPrice = (value: number | string | null | undefined) => {
if (value === null || value === undefined || value === "") {
return null;
}
const parsed = Number(value);
return Number.isFinite(parsed) && parsed >= 0 ? parsed : null;
};
const formatImagePricePreview = (value: number | string | null | undefined) => {
if (value === null || value === undefined || value === "") {
return t("admin.groups.imagePricing.notConfigured");
@@ -4300,10 +4313,12 @@ const buildImageFinalPricePreview = (form: ImagePricingFormState) => {
: normalizePreviewNumber(form.rate_multiplier, 1);
const multiplier = imageMultiplier;
return imagePricingTiers.map((tier) => {
const basePrice = normalizePreviewNumber(form[tier.key]);
const basePrice =
parsePreviewPrice(form[tier.key]) ??
getDefaultImagePreviewPrice(form.platform, tier.key);
return {
label: tier.label,
value: basePrice > 0
value: basePrice !== null
? formatImagePricePreview(basePrice * multiplier)
: t("admin.groups.imagePricing.notConfigured"),
};
@@ -4315,10 +4330,12 @@ const buildVideoFinalPricePreview = (form: VideoPricingFormState) => {
? normalizePreviewNumber(form.video_rate_multiplier, 1)
: normalizePreviewNumber(form.rate_multiplier, 1);
return videoPricingTiers.map((tier) => {
const basePrice = normalizePreviewNumber(form[tier.key]);
const basePrice =
parsePreviewPrice(form[tier.key]) ??
getDefaultVideoPreviewPrice(form.platform, tier.key);
return {
label: tier.label,
value: basePrice > 0
value: basePrice !== null
? formatVideoPricePreview(basePrice * multiplier)
: t("admin.groups.videoPricing.notConfigured"),
};
@@ -1,6 +1,10 @@
import { describe, expect, it } from "vitest";
import {
getDefaultImagePreviewPrice,
getDefaultVideoPreviewPrice,
getImagePricePlaceholder,
getVideoPricePlaceholder,
imagePricingPlatforms,
imagePricingI18nKey,
supportsImagePricingPlatform,
@@ -29,4 +33,18 @@ describe("groups image pricing platform support", () => {
);
expect(videoPricingI18nKey("title")).toBe("admin.groups.videoPricing.title");
});
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");
expect(getVideoPricePlaceholder("grok", "video_price_1080p")).toBe("0.25");
});
it("keeps non-Grok image placeholders on the generic image card", () => {
expect(getImagePricePlaceholder("openai", "image_price_1k")).toBe("0.134");
expect(getDefaultImagePreviewPrice("openai", "image_price_2k")).toBe(0.201);
expect(getDefaultVideoPreviewPrice("openai", "video_price_480p")).toBeNull();
});
});
@@ -16,3 +16,76 @@ export const imagePricingI18nKey = (_platform: string, key: string): string =>
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",
},
};
const defaultVideoPricePlaceholders: Record<
string,
Record<VideoPricingTierKey, string>
> = {
grok: {
video_price_480p: "0.08",
video_price_720p: "0.14",
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;
};