Skip to content

fix(billing): channel pricing override for image generation + display image_output_tokens#2930

Open
touwaeriol wants to merge 2 commits into
Wei-Shaw:mainfrom
touwaeriol:feat/image-token-billing
Open

fix(billing): channel pricing override for image generation + display image_output_tokens#2930
touwaeriol wants to merge 2 commits into
Wei-Shaw:mainfrom
touwaeriol:feat/image-token-billing

Conversation

@touwaeriol
Copy link
Copy Markdown
Contributor

背景 / Background

按 token 计费的图片生成模型(如 gemini-3.x image-preview)存在两个相互配套的问题:

  1. 后端定价:当渠道配置 token 定价(含图片输出价)时,ImageOutputPricePerToken 未真正覆盖默认值——若渠道显式配置为 0,会回退到 LiteLLM 或 group config 的价格;同时区间定价 fallback 到 BasePricing 时图片价格也漏覆盖。
  2. 前端展示:按 token 计费的图片请求在用量页仍按"1 张 (2K)"展示,API 响应缺 image_output_tokens / image_output_cost 字段,前端无法显示 token 拆分。

Image generation models billed per token (e.g. gemini-3.x image-preview) had two related issues:

  1. Backend pricing: When a channel configures token pricing (including image output price), ImageOutputPricePerToken was not fully overriding the default sources — explicit zero on the channel would fall back to LiteLLM/group config; interval fallback to BasePricing also missed the image price.
  2. Frontend display: Token-billed image requests still showed as "1 image (2K)" on usage pages, and the API response was missing image_output_tokens / image_output_cost fields needed for the breakdown.

目的 / Purpose

让渠道定价完全主导按 token 计费的图片生成计费,并在用量页面正确展示 token / 费用拆分。

Make channel pricing the single source of truth for token-billed image generation, and properly display the token/cost breakdown on usage pages.


改动内容 / Changes

后端 / Backend

  • 新增 ImageOutputPriceExplicit 标志billing_service.go):区分"渠道显式配置图片输出价"和"未配置缺失值"。true 时即使价格为 0 也不回退到 outputPrice
  • GetModelPricingWithChannel:渠道存在时,未配置的 ImageOutputPrice 归零并设 Explicit=true(不再回退 LiteLLM)
  • computeTokenBreakdown:用 Explicit 标志决定是否回退 outputPrice
  • model_pricing_resolver.goapplyTokenOverrides flat / interval 两条路径都设 explicit + 归零;intervalToModelPricing 也接收 channel pricing 设 explicit
  • calculateRecordUsageCost / OpenAI calculateOpenAIRecordUsageCost:渠道定价为 token 模式时图片走 token 路径而非按图计费
  • buildRecordUsageLog + OpenAI RecordUsage:图片请求走 token 计费时 RateMultiplier 用普通 multiplier 而非 image multiplier
  • DTO 补字段UsageLog DTO 添加 image_output_tokensimage_output_cost(数据库/service 早有,DTO 漏映射)

  • ImageOutputPriceExplicit flag (billing_service.go): Distinguish "channel explicitly set image output price" from "missing value". When true, a zero price does NOT fall back to outputPrice
  • GetModelPricingWithChannel: With a channel present, missing ImageOutputPrice is zeroed and marked Explicit=true (no more LiteLLM fallback)
  • computeTokenBreakdown: Uses the Explicit flag to decide whether to fall back to outputPrice
  • model_pricing_resolver.go: Both flat and interval paths in applyTokenOverrides set explicit + zero; intervalToModelPricing also receives channel pricing and sets explicit
  • calculateRecordUsageCost / OpenAI calculateOpenAIRecordUsageCost: When channel pricing is token-mode, image requests go through the token path instead of per-image billing
  • buildRecordUsageLog + OpenAI RecordUsage: When a token-billed image request is recorded, RateMultiplier uses the regular multiplier rather than the image multiplier
  • DTO field addition: Add image_output_tokens and image_output_cost to UsageLog DTO (already existed in DB/service, missing in DTO mapping)

前端 / Frontend

  • 计费模式感知isImageUsage() 现在检查 billing_mode !== 'token',token 计费的图片请求走 token 展示路径而非图片格式
  • Token 明细 Tooltip:拆分"输出 Token"为"输出 Token"(文本部分)+ "图片输出 Token"(粉色高亮),纯图片输出时隐藏文本行
  • 费用 Tooltip:新增图片输出成本行和图片输出单价(每百万 Token)
  • Token 列:按量计费的图片请求显示完整 token 明细,含粉色图片图标
  • 共享工具函数isImageUsagehasImageOutputTokenstextOutputTokens 抽到 @/utils/billingMode.ts@/utils/imageUsage.ts,Admin 和 User 页面共用
  • i18nimageOutput* 翻译统一到 usage.* 命名空间

  • Billing mode awareness: isImageUsage() now checks billing_mode !== 'token', routing token-billed image requests to the token display path
  • Token tooltip: Split "Output Tokens" into text output + "Image Output Tokens" (pink highlight); hide text row for pure-image output
  • Cost tooltip: Add image output cost row and image output unit price (per 1M tokens)
  • Token column: Show full token breakdown for token-billed image requests with a pink image icon
  • Shared utilities: Extract isImageUsage, hasImageOutputTokens, textOutputTokens to shared utils used by both Admin and User pages
  • i18n: Unify imageOutput* translation keys under usage.* namespace

测试 / Tests

后端新增 6 个单测覆盖:

  • TestGetModelPricingWithChannel_NilImageOutputPriceZerosAndMarksExplicit
  • TestComputeTokenBreakdown_ExplicitZeroImagePrice_NoFallback
  • TestComputeTokenBreakdown_NonExplicitZeroImagePrice_FallsBackToOutput
  • TestApplyTokenOverrides_FlatSetsImageOutputPriceExplicit
  • TestApplyTokenOverrides_FlatWithImageOutputPriceSetsExplicit
  • TestApplyTokenOverrides_IntervalSetsImageOutputPriceExplicit

全部 PASS。

Six new backend unit tests covering all four code paths (flat / interval / non-explicit fallback / explicit zero). All passing.


截图 / Screenshot

Token 列展示(按量计费的图片生成):

↓ 24  ↑ 1,514
🖼 1,120            ← 图片输出 Token(粉色)

Token 明细 Tooltip:

输入 Token         21
图片输出 Token   1,120    ← 粉色,与"输出 Token"区分
总 Token         1,141

费用 Tooltip:

输入成本         \$0.000012
图片输出成本     \$0.067200    ← 新增行
输入单价         \$0.5000 / 1M Token
图片输出单价     \$60.0000 / 1M Token  ← 新增行

备注 / Notes

This supersedes #2774 by bundling the backend billing fix with the frontend display changes; the prior PR's frontend-only scope depended on backend changes that hadn't landed.

When image generation models are billed by token (channel pricing mode=token),
the usage pages previously showed only image count format instead of detailed
token breakdown. This fixes the display to properly show image output tokens
separately from text output tokens.
When channel pricing is configured, image output token price now
correctly overrides all other sources (LiteLLM, group config).
Add ImageOutputPriceExplicit flag to distinguish intentional zero
price from missing price, preventing fallback to output price.

Affects Gemini, OpenAI inline image gen, and Images API paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant