diff --git a/backend/internal/service/billing_service.go b/backend/internal/service/billing_service.go index c13908443e..0356f698a0 100644 --- a/backend/internal/service/billing_service.go +++ b/backend/internal/service/billing_service.go @@ -862,6 +862,9 @@ func (s *BillingService) grokUnknownTextFamilyFallback(model string) *ModelPrici func isGrokUnknownTextFamilyModel(model string) bool { native := strings.ToLower(strings.TrimSpace(xai.StripGrokProviderPrefix(model))) + if isGrokMediaFamilyModel(native) { + return false + } switch { case native == "grok", native == "grok-latest": return true @@ -877,6 +880,19 @@ func isGrokUnknownTextFamilyModel(model string) bool { } } +// isGrokMediaFamilyModel matches ids that are billed per image/video/audio unit +// rather than per token, so version-numbered media ids (grok-2-image-1212, +// grok-5-video) cannot slip into the unknown-text fallback and pick up a token +// card. "vision" is deliberately absent: multimodal chat models are token billed. +func isGrokMediaFamilyModel(native string) bool { + for _, marker := range []string{"imagine", "image", "video", "audio", "speech", "tts", "transcribe", "realtime"} { + if strings.Contains(native, marker) { + return true + } + } + return false +} + // HasIdentifiedTokenPricing 判断模型能否在价格表中被"确定性识别"出 token 价格。 // // 与 GetModelPricing 的关键区别:本函数拒绝按子串猜系列的兜底。GetModelPricing 会 diff --git a/backend/internal/service/billing_service_test.go b/backend/internal/service/billing_service_test.go index d1ea67645c..80d07af44b 100644 --- a/backend/internal/service/billing_service_test.go +++ b/backend/internal/service/billing_service_test.go @@ -1221,6 +1221,14 @@ func TestGetModelPricing_UnknownGrokTextFallsBackToGrok45(t *testing.T) { require.InDelta(t, baseline.CacheReadPricePerToken, pricing.CacheReadPricePerToken, 1e-12, model) } + // Per-unit media ids must not inherit the text card just because they carry + // a version number; they are billed by the image/video/audio paths instead. + for _, model := range []string{"grok-2-image-1212", "grok-2-audio", "grok-5-video", "x-ai/grok-6-image"} { + require.False(t, isGrokUnknownTextFamilyModel(model), "model %s", model) + } + // Multimodal chat models stay token billed. + require.True(t, isGrokUnknownTextFamilyModel("grok-2-vision-1212")) + for _, model := range []string{ "grok-imagine-image-3.0", "grok-imagine-video-2", diff --git a/backend/internal/service/response_model_billing_test.go b/backend/internal/service/response_model_billing_test.go index 9194a1684e..6b04fc424a 100644 --- a/backend/internal/service/response_model_billing_test.go +++ b/backend/internal/service/response_model_billing_test.go @@ -372,6 +372,12 @@ func TestBillingServiceHasIdentifiedTokenPricing_RejectsFamilyGuesses(t *testing } require.False(t, billing.HasIdentifiedTokenPricing("")) require.False(t, billing.HasIdentifiedTokenPricing("zz-unpriced-response-model")) + // Versioned media ids may inherit a text card via GetModelPricing; the + // identified-token gate must still reject them so response-model billing + // cannot adopt grok-4.5 rates for image/audio/video ids. + require.False(t, billing.HasIdentifiedTokenPricing("grok-2-image-1212")) + require.False(t, billing.HasIdentifiedTokenPricing("grok-2-audio")) + require.False(t, billing.HasIdentifiedTokenPricing("grok-5-video")) } func TestGatewayServiceRecordUsage_ResponseModelRejectsUnidentifiedFamilyName(t *testing.T) {