mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
fix: update Grok media default rate card
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user