From 7a3ac2d7a9253c4e816693214008ed3f0c4b72b6 Mon Sep 17 00:00:00 2001 From: Chenyme <118253778+chenyme@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:06:36 +0800 Subject: [PATCH 1/2] fix: clarify token and video usage in model call logs --- .../internal/application/billing/service.go | 10 ++ .../billing/service_model_identity_test.go | 9 ++ .../conversation/service_billing.go | 11 ++- .../components/sections/logs/admin-logs.tsx | 95 ++++++++++++++++--- .../chat/components/message/message-bot.tsx | 21 +--- frontend/i18n/messages/en-US/admin-logs.json | 9 +- frontend/i18n/messages/en-US/chat.json | 1 - frontend/i18n/messages/zh-CN/admin-logs.json | 9 +- frontend/i18n/messages/zh-CN/chat.json | 1 - .../components/file-preview/preview-media.tsx | 5 +- 10 files changed, 127 insertions(+), 44 deletions(-) diff --git a/backend/internal/application/billing/service.go b/backend/internal/application/billing/service.go index 2371daa2..0f2e2b19 100644 --- a/backend/internal/application/billing/service.go +++ b/backend/internal/application/billing/service.go @@ -151,6 +151,8 @@ type UsagePricingInput struct { CallCount int64 DurationBillable bool DurationSeconds int64 + MediaType string + InputImageCount int64 LatencyMS int64 ServerSideToolUsage map[string]int64 ServiceItems []ServiceUsageInput @@ -1768,6 +1770,14 @@ func (s *Service) BuildUsageLedger(ctx context.Context, input UsagePricingInput) "base_service_billed_nanousd": serviceBilledNanousd, "service_items": usageServiceItemSnapshots(serviceItems), } + if strings.EqualFold(strings.TrimSpace(input.MediaType), "video") { + inputImageCount := input.InputImageCount + if inputImageCount < 0 { + inputImageCount = 0 + } + snapshot["media_type"] = "video" + snapshot["input_image_count"] = inputImageCount + } if usageSource := strings.TrimSpace(input.UsageSource); usageSource != "" { snapshot["usage_source"] = usageSource } diff --git a/backend/internal/application/billing/service_model_identity_test.go b/backend/internal/application/billing/service_model_identity_test.go index 3d221627..ac23f70c 100644 --- a/backend/internal/application/billing/service_model_identity_test.go +++ b/backend/internal/application/billing/service_model_identity_test.go @@ -101,6 +101,8 @@ func TestBuildUsageLedgerBillsDurationOnlyWhenExplicitlyBillable(t *testing.T) { PlatformModelName: "video-model", DurationBillable: true, DurationSeconds: 6, + MediaType: "video", + InputImageCount: 1, }) if err != nil { t.Fatalf("build video duration ledger: %v", err) @@ -108,6 +110,13 @@ func TestBuildUsageLedgerBillsDurationOnlyWhenExplicitlyBillable(t *testing.T) { if video.DurationSeconds != 6 || video.BilledNanousd != 18 { t.Fatalf("unexpected video duration billing: %#v", video) } + var snapshot map[string]interface{} + if err := json.Unmarshal([]byte(video.PricingSnapshotJSON), &snapshot); err != nil { + t.Fatalf("unmarshal video pricing snapshot: %v", err) + } + if snapshot["media_type"] != "video" || snapshot["input_image_count"] != float64(1) { + t.Fatalf("unexpected video media snapshot: %#v", snapshot) + } } func TestAuthorizeUsageRejectsLegacyDurationPricingForNonVideoModel(t *testing.T) { diff --git a/backend/internal/application/conversation/service_billing.go b/backend/internal/application/conversation/service_billing.go index 88cfd6cb..dfa90e4c 100644 --- a/backend/internal/application/conversation/service_billing.go +++ b/backend/internal/application/conversation/service_billing.go @@ -278,6 +278,13 @@ func (s *Service) buildSendMessageUsageLedger(ctx context.Context, input SendMes if result == nil { return nil, nil } + isVideoGeneration := sendMessageResultIsVideoGeneration(result) + mediaType := "" + inputImageCount := int64(0) + if isVideoGeneration { + mediaType = "video" + inputImageCount, _ = countAttachmentKinds(result.UserMessage.Attachments) + } latencyMS := result.LatencyMS if latencyMS <= 0 { latencyMS = result.AssistantMessage.LatencyMS @@ -305,8 +312,10 @@ func (s *Service) buildSendMessageUsageLedger(ctx context.Context, input SendMes OutputTokens: result.AssistantMessage.OutputTokens, ReasoningTokens: result.AssistantMessage.ReasoningTokens, CallCount: 1, - DurationBillable: sendMessageResultIsVideoGeneration(result), + DurationBillable: isVideoGeneration, DurationSeconds: sendMessageBillingDurationSeconds(result), + MediaType: mediaType, + InputImageCount: inputImageCount, LatencyMS: latencyMS, ServerSideToolUsage: result.ServerSideToolUsage, RawUsageJSON: result.RawUsageJSON, diff --git a/frontend/features/admin/components/sections/logs/admin-logs.tsx b/frontend/features/admin/components/sections/logs/admin-logs.tsx index 9039f630..ccf39e7f 100644 --- a/frontend/features/admin/components/sections/logs/admin-logs.tsx +++ b/frontend/features/admin/components/sections/logs/admin-logs.tsx @@ -167,13 +167,20 @@ function formatUsageBalance(value: number | null | undefined, billingDisplay: Bi return value === null || value === undefined ? "-" : formatBillingBalance(value, billingDisplay); } +function usageBillableOutputTokens(item: AdminUsageLogDTO): number { + return item.outputTokens + item.reasoningTokens; +} + function usageTotalTokens(item: AdminUsageLogDTO): number { - return item.inputTokens + item.cacheReadTokens + item.cacheWriteTokens + item.outputTokens + item.reasoningTokens; + return item.inputTokens + item.cacheReadTokens + item.cacheWriteTokens + usageBillableOutputTokens(item); } type UsagePricingSnapshot = { pricing_mode?: "token" | "call" | "duration" | "tiered" | string; provider_protocol?: string; + duration_billable?: boolean; + media_type?: string; + input_image_count?: number; cache_timeout?: string; fast_mode?: boolean; billing_speed?: string; @@ -494,7 +501,7 @@ function buildUsageBillingTooltipLines( const outputRate = readUsageSnapshotNumber(snapshot, "output_nanousd_per_m_tokens"); const cacheReadRate = readUsageSnapshotNumber(snapshot, "cache_read_nanousd_per_m_tokens"); const cacheWriteRate = readUsageSnapshotNumber(snapshot, "cache_write_nanousd_per_m_tokens"); - const billedOutputTokens = item.outputTokens + item.reasoningTokens; + const billedOutputTokens = usageBillableOutputTokens(item); const totalLine = usageTotalLine(item, labels, billingDisplay); const cacheWriteLabel = cacheWriteBillingLabel(snapshot, labels.billingDisplay); const cacheWriteNote = cacheWriteBillingNote(snapshot, labels.billingDisplay); @@ -604,26 +611,84 @@ function UsageLogModelCell({ item, labels }: { item: AdminUsageLogDTO; labels: U ); } -function UsageLogTokenCell({ item, locale }: { item: AdminUsageLogDTO; locale: string }) { +function UsageLogUsageCell({ item, locale }: { item: AdminUsageLogDTO; locale: string }) { const t = useTranslations("adminLogs.usage.tokens"); + const snapshot = parseUsagePricingSnapshot(item.pricingSnapshotJSON); + const isVideoUsage = snapshot.media_type === "video" || snapshot.duration_billable === true; + if (isVideoUsage) { + const inputImageCount = typeof snapshot.input_image_count === "number" && Number.isFinite(snapshot.input_image_count) && snapshot.input_image_count >= 0 + ? Math.trunc(snapshot.input_image_count) + : null; + const mediaUsage = [ + { label: t("input"), value: inputImageCount === null ? "—" : t("imageCount", { count: inputImageCount }) }, + { label: t("output"), value: t("secondCount", { count: item.durationSeconds }) }, + ]; + return ( +