Skip to content

feat(billing): support dynamic media model pricing - #6054

Closed
twodogegg wants to merge 2 commits into
QuantumNous:mainfrom
twodogegg:feat/media-model-resolution-billing
Closed

feat(billing): support dynamic media model pricing#6054
twodogegg wants to merge 2 commits into
QuantumNous:mainfrom
twodogegg:feat/media-model-resolution-billing

Conversation

@twodogegg

@twodogegg twodogegg commented Jul 10, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

本 PR 的代码与说明由 Codex AI 辅助生成,提交者已在本地执行自动化测试和浏览器验证。请维护者按 AI-assisted contribution 审核。

📝 变更描述 / Description

为图片与视频模型补充可配置的动态按次计费:图片按输出分辨率及可选质量矩阵定价,视频按输出分辨率和生成时长定价。预扣费、结算和日志共享同一价格档位与分组倍率,并使用统一额度饱和转换,避免重复计费、额度溢出和先调用后发现余额不足。

模型广场、价格详情、价格表与用量日志会识别这两类计费方式,并按照系统配置的人民币兑美元汇率展示价格。管理端模型定价编辑器支持配置、编辑和预览图片分辨率价格与视频每秒价格。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature)
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 本 PR 为 AI-assisted contribution,已在顶部明确披露;提交者仍需人工复核描述与代码。
  • 非重复提交: 已检查 feat(billing): add per-resolution image model billing #5035,并在上方说明功能重叠及本 PR 的新增范围。
  • Bug fix 说明: 不适用,本 PR 作为功能扩展提交。
  • 变更理解: 已核对预扣费、结算、日志和价格展示链路。
  • 范围聚焦: 仅包含媒体模型动态计费、配置、展示、日志与对应测试。
  • 本地验证: 已运行后端测试、前端类型检查、lint、格式检查、单元测试、生产构建和浏览器验证。
  • 安全合规: 代码中无敏感凭据,额度转换遵循项目饱和与审计规则。

📸 运行证明 / Proof of Work

  • go test ./relay/helper ./setting/operation_setting ./service ./relay
  • bun run typecheck
  • bun test tests/pricing-display.test.tsx
  • PR 涉及文件通过 oxlintoxfmt --check
  • bun run build
  • 浏览器验证:人民币模式下,gpt-image-2 显示 1K ¥0.05 / 2K ¥0.08 / 4K ¥0.15grok-imagine-video 显示 480P ¥0.09 / 720P ¥0.15 / 1080P ¥0.36 / 4K ¥0.55

Summary by CodeRabbit

  • New Features
    • Added layered configuration for image per-size billing and video per-second billing, including index rebuild and pricing cache invalidation.
    • Implemented per-size image and per-second video quota/pricing calculations (with tier/matrix + default-seconds fallback).
    • Updated pricing/admin UI to display the new modes with correct /image and /sec units, plus usage log breakdowns and details.
    • Added localized UI text for the new billing options.
  • Bug Fixes
    • Prevented adaptor-based ratio merging/overwrites in video-per-second mode to preserve the computed final quota.
  • Tests
    • Added coverage for image/video pricing fallback logic and pricing-display formatting.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

This change adds configurable per-resolution image billing and per-second video billing across backend pricing, quota settlement, administration, pricing displays, usage logs, localization, and validation tests.

Changes

Media billing configuration and settlement

Layer / File(s) Summary
Billing configuration and pricing contracts
setting/operation_setting/*, model/pricing.go, model/option.go
Adds normalized image/video billing settings, fallback pricing helpers, cache rebuilds, and pricing payload fields.
Media request quota settlement
relay/helper/price.go, relay/relay_task.go, service/text_quota.go, relay/image_handler.go
Calculates image per-size and video per-second charges, preserves finalized quotas, records billing metadata, and validates fallback and clamp behavior.

Pricing administration and presentation

Layer / File(s) Summary
Pricing administration and model snapshots
web/default/src/features/system-settings/models/*
Adds fixed, per-resolution, and per-second pricing submodes to forms, previews, snapshots, persistence, and model settings.
Pricing display and currency formatting
web/default/src/features/pricing/*, web/default/tests/pricing-display.test.tsx
Displays image resolution tiers and video resolution matrices with currency conversion and tests the rendered values.
Billing audit and localized presentation
web/default/src/features/usage-logs/*, web/default/src/i18n/locales/*, web/default/src/hooks/use-system-config.ts
Shows image billing details in usage logs, normalizes currency display settings, and adds media-pricing translations.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: Calcium-Ion, seefs001

Poem

I’m a rabbit with prices in rows,
Counting each image as it grows.
Seconds for videos, tiers in a line,
Configured matrices make billing shine.
Cache hops, quotas thump, displays glow—
Hop-hop, the media charges flow!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Clear and relevant; it accurately summarizes the main change: dynamic pricing for image and video media models.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (5)
web/default/src/hooks/use-status.ts (1)

78-89: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Avoid synchronizing system config twice per fetch.

The query function already calls setConfig at Lines [46]-[50]. This effect calls it again when the same result becomes data, causing duplicate store updates and possible unnecessary rerenders. Centralize synchronization in one path that handles both cached and fetched data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/hooks/use-status.ts` around lines 78 - 89, Remove the
duplicate synchronization logic from the useStatus effect, since the query
function already invokes setConfig for fetched and cached results. Keep a single
synchronization path in the query function, including the mapStatusDataToConfig
conversion, and delete the effect and its related warning code.
web/default/src/hooks/use-system-config.ts (1)

45-45: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Widened type loses compile-time safety without added runtime validation.

quota_display_type is now typed as a plain string (matching the backend contract more accurately, since the backend can technically return values outside the enum), but mapStatusDataToConfig still blindly casts it via data.quota_display_type as CurrencyDisplayType | undefined with no validation against the known CurrencyDisplayType values. Previously the stricter type at least signaled intent; now an unexpected string from the backend flows through unchecked to quotaDisplayType, and any invalid value will only surface downstream when consumed for display formatting.

Consider validating against the known enum values (e.g., a small allow-list check) and falling back to DEFAULT_CURRENCY_CONFIG.quotaDisplayType when the value doesn't match.

🛡️ Proposed validation
+const VALID_QUOTA_DISPLAY_TYPES: CurrencyDisplayType[] = ['USD', 'CNY', 'TOKENS', 'CUSTOM']
+
 const quotaDisplayType =
-  (data.quota_display_type as CurrencyDisplayType | undefined) ??
-  DEFAULT_CURRENCY_CONFIG.quotaDisplayType
+  (VALID_QUOTA_DISPLAY_TYPES.includes(data.quota_display_type as CurrencyDisplayType)
+    ? (data.quota_display_type as CurrencyDisplayType)
+    : undefined) ?? DEFAULT_CURRENCY_CONFIG.quotaDisplayType

Also applies to: 70-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/hooks/use-system-config.ts` at line 45, mapStatusDataToConfig
must validate the widened quota_display_type string before assigning
quotaDisplayType. Add an allow-list check for the valid CurrencyDisplayType
values and use DEFAULT_CURRENCY_CONFIG.quotaDisplayType for undefined or invalid
values, removing the unchecked cast.
setting/operation_setting/media_model_setting_test.go (1)

43-85: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use testify assertions for these new tests.

These are new backend tests but use hand-rolled t.Fatalf comparisons. Per the repository guideline, new Go tests should use require for fatal assertions and assert for value checks. The table structure and contracts are good; only the assertion style needs updating.

♻️ Example for the image tier table
-			price, key := GetImageTierPrice("image-test", test.size, test.quality)
-			if price != test.price || key != test.key {
-				t.Fatalf("got (%v, %q), want (%v, %q)", price, key, test.price, test.key)
-			}
+			price, key := GetImageTierPrice("image-test", test.size, test.quality)
+			assert.Equal(t, test.price, price)
+			assert.Equal(t, test.key, key)

As per coding guidelines: "New or substantially rewritten Go backend tests must use github.com/stretchr/testify/require for setup and fatal assertions, and github.com/stretchr/testify/assert for non-fatal value checks."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@setting/operation_setting/media_model_setting_test.go` around lines 43 - 85,
Replace the hand-rolled t.Fatalf comparisons in the new tests, including
TestVideoPerSecondPriceFallbacks and the image-tier table test, with testify
assertions: use assert.Equal for returned values and keys, and require where
setup or fatal conditions are appropriate. Add the required testify imports
while preserving the existing test cases and cleanup behavior.

Source: Coding guidelines

web/default/src/features/pricing/components/model-card.tsx (1)

324-330: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the billing-mode label to avoid a 3-level nested ternary.

The footer label chain (isVideoPerSecond ? ... : isImagePerSize ? ... : isTokenBased ? ... : ...) is three levels deep, violating the guideline: "Avoid nested ternary expressions two levels deep or more; use if/else, early returns, or extracted helper functions instead."

As per coding guidelines for web/default/**/*.{ts,tsx} files.

♻️ Suggested refactor: extract a helper function
+function getBillingModeLabel(
+  isVideoPerSecond: boolean,
+  isImagePerSize: boolean,
+  isTokenBased: boolean,
+  t: (key: string) => string
+): string {
+  if (isVideoPerSecond) return t('Per-second')
+  if (isImagePerSize) return t('Per-resolution')
+  if (isTokenBased) return t('Token-based')
+  return t('Per Request')
+}

Then use it at the call site:

           <span className='text-muted-foreground text-xs font-medium'>
-            {isVideoPerSecond
-              ? t('Per-second')
-              : isImagePerSize
-                ? t('Per-resolution')
-                : isTokenBased
-                  ? t('Token-based')
-                  : t('Per Request')}
+            {getBillingModeLabel(isVideoPerSecond, isImagePerSize, isTokenBased, t)}
           </span>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-card.tsx` around lines 324
- 330, Extract the nested billing-mode ternary from the footer in the model card
component into a named helper function that selects the translated label using
clear conditional logic, then replace the inline expression with a call to that
helper while preserving the existing precedence and fallback label.

Source: Coding guidelines

web/default/src/features/pricing/components/model-details.tsx (1)

1149-1179: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the group-pricing column builder to avoid a 3-level nested ternary.

The column spread (isImagePerSize ? ... : isVideoPerSecond ? ... : isTokenBased ? ... : ...) is three levels deep, violating the guideline: "Avoid nested ternary expressions two levels deep or more; use if/else, early returns, or extracted helper functions instead."

As per coding guidelines for web/default/**/*.{ts,tsx} files.

♻️ Suggested refactor: extract a helper that returns the billing-mode columns
+function buildBillingColumns(
+  isImagePerSize: boolean,
+  isVideoPerSecond: boolean,
+  isTokenBased: boolean,
+  imagePriceEntries: ImageSummaryPriceEntry[],
+  videoPriceEntries: Array<[string, number]>,
+  extraPriceTypes: { label: string; type: PriceType }[],
+  thClass: string,
+  showRechargePrice: boolean,
+  props: GroupPricingSection['props'],
+  renderGroupPrice: (group: string, type: PriceType) => ReactNode,
+  renderFixedGroupPrice: (group: string) => ReactNode,
+  t: (key: string) => string
+) {
+  if (isImagePerSize) {
+    return imagePriceEntries.map((entry) => ({
+      id: entry.key,
+      header: entry.label,
+      className: `${thClass} text-right`,
+      cellClassName: 'py-2.5 text-right font-mono',
+      cell: (group: string) =>
+        formatUsdUnitPrice(
+          entry.value * (props.groupRatio[group] || 1),
+          showRechargePrice,
+          props.priceRate,
+          props.usdExchangeRate,
+          { digitsLarge: 3, digitsSmall: 3 }
+        ),
+    }))
+  }
+  if (isVideoPerSecond) {
+    return videoPriceEntries.map(([key, value]) => ({
+      id: key,
+      header: key,
+      className: `${thClass} text-right`,
+      cellClassName: 'py-2.5 text-right font-mono',
+      cell: (group: string) =>
+        formatUsdUnitPrice(
+          value * (props.groupRatio[group] || 1),
+          showRechargePrice,
+          props.priceRate,
+          props.usdExchangeRate,
+          { digitsLarge: 3, digitsSmall: 3 }
+        ),
+    }))
+  }
+  if (isTokenBased) {
+    return [
+      {
+        id: 'input',
+        header: t('Input'),
+        className: `${thClass} text-right`,
+        cellClassName: 'py-2.5 text-right font-mono',
+        cell: (group: string) => renderGroupPrice(group, 'input'),
+      },
+      {
+        id: 'output',
+        header: t('Output'),
+        className: `${thClass} text-right`,
+        cellClassName: 'py-2.5 text-right font-mono',
+        cell: (group: string) => renderGroupPrice(group, 'output'),
+      },
+      ...extraPriceTypes.map((ep) => ({
+        id: ep.type,
+        header: ep.label,
+        className: `${thClass} text-right`,
+        cellClassName: 'py-2.5 text-right font-mono',
+        cell: (group: string) => renderGroupPrice(group, ep.type),
+      })),
+    ]
+  }
+  return [
+    {
+      id: 'price',
+      header: t('Price'),
+      className: `${thClass} text-right`,
+      cellClassName: 'py-2.5 text-right font-mono',
+      cell: renderFixedGroupPrice,
+    },
+  ]
+}

Then replace the spread:

-          ...(isImagePerSize
-            ? imagePriceEntries.map(...)
-            : isVideoPerSecond
-              ? videoPriceEntries.map(...)
-              : isTokenBased
-            ? [...]
-            : [...]),
+          ...buildBillingColumns(
+            isImagePerSize,
+            isVideoPerSecond,
+            isTokenBased,
+            imagePriceEntries,
+            videoPriceEntries,
+            extraPriceTypes,
+            thClass,
+            showRechargePrice,
+            props,
+            renderGroupPrice,
+            renderFixedGroupPrice,
+            t
+          ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-details.tsx` around lines
1149 - 1179, Refactor the nested billing-mode ternary used by the group-pricing
column spread into a dedicated helper function near the surrounding
column-building logic. Have the helper return the appropriate columns for
image-per-size, video-per-second, token-based, and fallback modes using if/else
or early returns, then replace the ternary spread with the helper call while
preserving existing pricing calculations and column metadata.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx`:
- Around line 234-248: The per-resolution billing segment in the image pricing
logic should identify the compact price as per-image. Update the price text
appended in the image_per_size_billing branch within the relevant column
formatter to include the localized “price/image” unit, matching the details view
and existing localization conventions.

In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 313-320: The generic billing-mode logic can incorrectly add a
“Per-token” row for per-size image billing when model_price is absent. In the
details dialog’s row-building logic, detect `other.image_per_size_billing`
before the generic mode-selection branch, or skip that branch for this mode, so
per-size image entries only render the image-generation row.

In `@web/default/src/features/usage-logs/types.ts`:
- Around line 190-194: Extend the frontend audit contract by adding
image_quality_tier and image_price_tier_key to LogOtherData, matching the fields
serialized by service/text_quota.go. Update both billing views to render these
quality-tier metadata fields alongside the existing image per-size billing
details.

---

Nitpick comments:
In `@setting/operation_setting/media_model_setting_test.go`:
- Around line 43-85: Replace the hand-rolled t.Fatalf comparisons in the new
tests, including TestVideoPerSecondPriceFallbacks and the image-tier table test,
with testify assertions: use assert.Equal for returned values and keys, and
require where setup or fatal conditions are appropriate. Add the required
testify imports while preserving the existing test cases and cleanup behavior.

In `@web/default/src/features/pricing/components/model-card.tsx`:
- Around line 324-330: Extract the nested billing-mode ternary from the footer
in the model card component into a named helper function that selects the
translated label using clear conditional logic, then replace the inline
expression with a call to that helper while preserving the existing precedence
and fallback label.

In `@web/default/src/features/pricing/components/model-details.tsx`:
- Around line 1149-1179: Refactor the nested billing-mode ternary used by the
group-pricing column spread into a dedicated helper function near the
surrounding column-building logic. Have the helper return the appropriate
columns for image-per-size, video-per-second, token-based, and fallback modes
using if/else or early returns, then replace the ternary spread with the helper
call while preserving existing pricing calculations and column metadata.

In `@web/default/src/hooks/use-status.ts`:
- Around line 78-89: Remove the duplicate synchronization logic from the
useStatus effect, since the query function already invokes setConfig for fetched
and cached results. Keep a single synchronization path in the query function,
including the mapStatusDataToConfig conversion, and delete the effect and its
related warning code.

In `@web/default/src/hooks/use-system-config.ts`:
- Line 45: mapStatusDataToConfig must validate the widened quota_display_type
string before assigning quotaDisplayType. Add an allow-list check for the valid
CurrencyDisplayType values and use DEFAULT_CURRENCY_CONFIG.quotaDisplayType for
undefined or invalid values, removing the unchecked cast.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4ee87d27-8b84-4bd0-bdbd-2b95cfe219fe

📥 Commits

Reviewing files that changed from the base of the PR and between 246d62a and d2b1f0c.

📒 Files selected for processing (37)
  • model/option.go
  • model/pricing.go
  • relay/image_handler.go
  • relay/relay_task.go
  • service/text_quota.go
  • setting/operation_setting/image_model_setting.go
  • setting/operation_setting/media_model_setting_test.go
  • setting/operation_setting/video_model_setting.go
  • web/default/src/features/pricing/components/model-card.tsx
  • web/default/src/features/pricing/components/model-details.tsx
  • web/default/src/features/pricing/components/pricing-columns.tsx
  • web/default/src/features/pricing/lib/model-helpers.ts
  • web/default/src/features/pricing/lib/price.ts
  • web/default/src/features/pricing/types.ts
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • web/default/src/features/system-settings/models/model-pricing-core.ts
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-pricing-snapshots.ts
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/system-settings/models/model-ratio-table-columns.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/hooks/use-status.ts
  • web/default/src/hooks/use-system-config.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json
  • web/default/tests/pricing-display.test.tsx

Comment thread web/default/src/features/usage-logs/types.ts
@twodogegg
twodogegg force-pushed the feat/media-model-resolution-billing branch from d2b1f0c to feb3f66 Compare July 10, 2026 08:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
relay/helper/price_test.go (1)

58-63: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use assert for the independent value checks.

Per coding guidelines, non-fatal value checks in Go tests should use testify/assert, reserving require for setup/fatal assertions. Lines 58-63 and 76-79 check multiple independent properties of the same result with require, so a single failing assertion masks the rest.

As per coding guidelines, "New or substantially rewritten Go backend tests must use github.com/stretchr/testify/require for setup and fatal assertions, and github.com/stretchr/testify/assert for non-fatal value checks."

♻️ Proposed fix
-	priceData, err := ModelPriceHelper(ctx, info, 0, request.GetTokenCountMeta())
-	require.NoError(t, err)
-	require.Equal(t, 80000, priceData.QuotaToPreConsume)
-	require.Equal(t, 0.08, priceData.ModelPrice)
-	require.True(t, ctx.GetBool("image_per_size_billing"))
-	require.Equal(t, 2, ctx.GetInt("image_per_size_count"))
-	require.Equal(t, "1k_high", ctx.GetString("image_price_tier_key"))
+	priceData, err := ModelPriceHelper(ctx, info, 0, request.GetTokenCountMeta())
+	require.NoError(t, err)
+	assert.Equal(t, 80000, priceData.QuotaToPreConsume)
+	assert.Equal(t, 0.08, priceData.ModelPrice)
+	assert.True(t, ctx.GetBool("image_per_size_billing"))
+	assert.Equal(t, 2, ctx.GetInt("image_per_size_count"))
+	assert.Equal(t, "1k_high", ctx.GetString("image_price_tier_key"))

(similarly for lines 76-79, and add "github.com/stretchr/testify/assert" to imports)

Also applies to: 75-79

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@relay/helper/price_test.go` around lines 58 - 63, Replace the independent
value checks in the affected test cases with testify/assert calls, while keeping
require.NoError for setup or fatal assertions. Update the imports to include
github.com/stretchr/testify/assert, and apply this to both the checks around
priceData and the corresponding checks around lines 75-79.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@relay/helper/price_test.go`:
- Around line 58-63: Replace the independent value checks in the affected test
cases with testify/assert calls, while keeping require.NoError for setup or
fatal assertions. Update the imports to include
github.com/stretchr/testify/assert, and apply this to both the checks around
priceData and the corresponding checks around lines 75-79.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d6819d63-485a-4bf8-9d26-4214407cb005

📥 Commits

Reviewing files that changed from the base of the PR and between d2b1f0c and feb3f66.

📒 Files selected for processing (38)
  • model/option.go
  • model/pricing.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • relay/image_handler.go
  • relay/relay_task.go
  • service/text_quota.go
  • setting/operation_setting/image_model_setting.go
  • setting/operation_setting/media_model_setting_test.go
  • setting/operation_setting/video_model_setting.go
  • web/default/src/features/pricing/components/model-card.tsx
  • web/default/src/features/pricing/components/model-details.tsx
  • web/default/src/features/pricing/components/pricing-columns.tsx
  • web/default/src/features/pricing/lib/model-helpers.ts
  • web/default/src/features/pricing/lib/price.ts
  • web/default/src/features/pricing/types.ts
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • web/default/src/features/system-settings/models/model-pricing-core.ts
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-pricing-snapshots.ts
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/system-settings/models/model-ratio-table-columns.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/hooks/use-system-config.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json
  • web/default/tests/pricing-display.test.tsx
✅ Files skipped from review due to trivial changes (5)
  • web/default/src/i18n/locales/zh-TW.json
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/en.json
🚧 Files skipped from review as they are similar to previous changes (27)
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • web/default/src/features/system-settings/models/model-ratio-table-columns.tsx
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/tests/pricing-display.test.tsx
  • model/option.go
  • web/default/src/hooks/use-system-config.ts
  • relay/relay_task.go
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/pricing/lib/price.ts
  • web/default/src/features/pricing/types.ts
  • web/default/src/features/pricing/components/model-details.tsx
  • web/default/src/features/pricing/components/pricing-columns.tsx
  • model/pricing.go
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • setting/operation_setting/video_model_setting.go
  • web/default/src/features/system-settings/models/model-pricing-core.ts
  • setting/operation_setting/media_model_setting_test.go
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • service/text_quota.go
  • web/default/src/i18n/locales/fr.json
  • web/default/src/features/pricing/lib/model-helpers.ts
  • web/default/src/i18n/locales/vi.json
  • web/default/src/features/pricing/components/model-card.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • setting/operation_setting/image_model_setting.go
  • web/default/src/features/system-settings/models/model-pricing-snapshots.ts

@twodogegg
twodogegg force-pushed the feat/media-model-resolution-billing branch 2 times, most recently from 6ddad8a to f705bb5 Compare July 11, 2026 12:12

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/relay_task.go`:
- Around line 222-232: The otherRatios construction in ResolveOriginTask must
not persist or replay the seconds value, because it can be applied as a billing
multiplier for non-per-second models. Remove seconds from the persisted map, or
ensure replay through BillingContext.OtherRatios is gated to per-second billing
while preserving resolution_tier logging metadata.
- Around line 237-246: Update the PriceData assignment in the task pricing flow
around info.PriceData and the task-log/usage-log formatters so video task
unitPrice is represented as per-second pricing without being stored as a
positive model_price interpreted as per-call billing. Add and propagate a
dedicated per-second pricing indicator, or branch formatting by task type,
ensuring displayed billing mode and amount remain correct while preserving
non-video pricing behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 84c8b72e-e02f-4a61-a01d-9872fe38463f

📥 Commits

Reviewing files that changed from the base of the PR and between 6ddad8a and f705bb5.

📒 Files selected for processing (10)
  • model/option.go
  • model/pricing.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • relay/image_handler.go
  • relay/relay_task.go
  • service/text_quota.go
  • setting/operation_setting/image_model_setting.go
  • setting/operation_setting/media_model_setting_test.go
  • setting/operation_setting/video_model_setting.go
🚧 Files skipped from review as they are similar to previous changes (9)
  • relay/image_handler.go
  • setting/operation_setting/media_model_setting_test.go
  • model/option.go
  • relay/helper/price_test.go
  • service/text_quota.go
  • setting/operation_setting/video_model_setting.go
  • model/pricing.go
  • relay/helper/price.go
  • setting/operation_setting/image_model_setting.go

Comment thread relay/relay_task.go
Comment thread relay/relay_task.go
@twodogegg
twodogegg force-pushed the feat/media-model-resolution-billing branch from f705bb5 to f5bd53e Compare July 11, 2026 12:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
web/default/src/features/pricing/components/model-details.tsx (1)

589-610: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicated mediaPriceEntries computation across PriceSection and GroupPricingSection. Both functions derive mediaPriceEntries with identical logic — the same isImagePerSize ternary, null-guard for image_per_size_prices, and mapping to { label, value }. Extract a shared helper so the two call sites stay in sync.

  • web/default/src/features/pricing/components/model-details.tsx#L589-L610: replace the inline mediaPriceEntries computation in PriceSection with a call to the extracted helper.
  • web/default/src/features/pricing/components/model-details.tsx#L926-L935: replace the inline mediaPriceEntries computation in GroupPricingSection with the same helper call.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-details.tsx` around lines
589 - 610, Extract the duplicated mediaPriceEntries derivation into a shared
helper in model-details.tsx, preserving the isImagePerSize check, null handling,
and { label, value } mapping. Replace the inline computation in PriceSection at
web/default/src/features/pricing/components/model-details.tsx:589-610 and
GroupPricingSection at
web/default/src/features/pricing/components/model-details.tsx:926-935 with calls
to that helper so both sites remain consistent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web/default/src/features/pricing/components/model-card.tsx`:
- Around line 109-158: Update the pricing summary branching around
isImagePerSize so image-per-size models always use the image pricing path, even
when image_per_size_prices is null or missing. Handle the absent price list by
rendering an empty entry list, matching the behavior used by model-details.tsx
and pricing-columns.tsx, while preserving the existing video and other pricing
branches.

---

Nitpick comments:
In `@web/default/src/features/pricing/components/model-details.tsx`:
- Around line 589-610: Extract the duplicated mediaPriceEntries derivation into
a shared helper in model-details.tsx, preserving the isImagePerSize check, null
handling, and { label, value } mapping. Replace the inline computation in
PriceSection at
web/default/src/features/pricing/components/model-details.tsx:589-610 and
GroupPricingSection at
web/default/src/features/pricing/components/model-details.tsx:926-935 with calls
to that helper so both sites remain consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f13e0356-5487-4b8d-88cb-588647794a1c

📥 Commits

Reviewing files that changed from the base of the PR and between f705bb5 and f5bd53e.

📒 Files selected for processing (37)
  • model/option.go
  • model/pricing.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • relay/image_handler.go
  • relay/relay_task.go
  • service/text_quota.go
  • setting/operation_setting/image_model_setting.go
  • setting/operation_setting/media_model_setting_test.go
  • setting/operation_setting/video_model_setting.go
  • web/default/src/features/pricing/components/model-card.tsx
  • web/default/src/features/pricing/components/model-details.tsx
  • web/default/src/features/pricing/components/pricing-columns.tsx
  • web/default/src/features/pricing/lib/model-helpers.ts
  • web/default/src/features/pricing/lib/price.ts
  • web/default/src/features/pricing/types.ts
  • web/default/src/features/system-settings/billing/index.tsx
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • web/default/src/features/system-settings/models/model-pricing-core.ts
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-pricing-snapshots.ts
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/src/features/system-settings/models/model-ratio-table-columns.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/hooks/use-system-config.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json
  • web/default/tests/pricing-display.test.tsx
✅ Files skipped from review due to trivial changes (3)
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/locales/ja.json
🚧 Files skipped from review as they are similar to previous changes (29)
  • relay/image_handler.go
  • web/default/src/features/system-settings/billing/index.tsx
  • model/option.go
  • web/default/src/features/system-settings/models/model-ratio-table-columns.tsx
  • relay/helper/price_test.go
  • web/default/src/hooks/use-system-config.ts
  • setting/operation_setting/media_model_setting_test.go
  • web/default/src/features/system-settings/models/model-ratio-form.tsx
  • web/default/tests/pricing-display.test.tsx
  • web/default/src/features/system-settings/types.ts
  • web/default/src/features/pricing/lib/model-helpers.ts
  • web/default/src/features/system-settings/models/ratio-settings-card.tsx
  • web/default/src/features/system-settings/billing/section-registry.tsx
  • relay/relay_task.go
  • web/default/src/features/pricing/lib/price.ts
  • relay/helper/price.go
  • setting/operation_setting/video_model_setting.go
  • model/pricing.go
  • web/default/src/i18n/locales/en.json
  • web/default/src/features/system-settings/models/model-pricing-core.ts
  • setting/operation_setting/image_model_setting.go
  • web/default/src/features/usage-logs/types.ts
  • web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/default/src/features/pricing/types.ts
  • web/default/src/i18n/locales/vi.json
  • service/text_quota.go
  • web/default/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/default/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/default/src/features/system-settings/models/model-pricing-snapshots.ts

Comment on lines +109 to +158
if (isImagePerSize && props.model.image_per_size_prices) {
priceSummary = (
<>
{getImageSummaryPriceEntries(props.model.image_per_size_prices).map(
(entry) => (
<span
key={entry.key}
className='text-muted-foreground whitespace-nowrap'
>
{entry.label}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatUsdUnitPrice(
entry.value,
showRechargePrice,
priceRate,
usdExchangeRate
)}
</span>
/{t('image')}
</span>
)
)}
</>
)
} else if (isVideoPerSecond) {
const videoEntries = getOrderedVideoPriceEntries(
getVideoPriceMatrix(props.model)
)
priceSummary = (
<>
{videoEntries.map(([resolution, price]) => (
<span
key={resolution}
className='text-muted-foreground whitespace-nowrap'
>
{resolution}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatUsdUnitPrice(
price,
showRechargePrice,
priceRate,
usdExchangeRate
)}
</span>
/{t('sec')}
</span>
))}
</>
)
} else if (dynamicSummary) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Image-per-size models with missing image_per_size_prices fall through to incorrect pricing display.

The condition isImagePerSize && props.model.image_per_size_prices at line 109 causes image-per-size models with null image_per_size_prices to skip the image rendering branch and fall through to the dynamic, token-based, or per-request pricing paths — potentially showing misleading pricing (e.g., a per-request price) for a model configured as per-size. In contrast, model-details.tsx and pricing-columns.tsx both guard with if (isImagePerSize || isVideoPerSecond) and handle the null case inside by mapping to an empty entry list.

Proposed fix to align with other components
-  if (isImagePerSize && props.model.image_per_size_prices) {
-    priceSummary = (
-      <>
-        {getImageSummaryPriceEntries(props.model.image_per_size_prices).map(
-          (entry) => (
+  if (isImagePerSize) {
+    const imageEntries = props.model.image_per_size_prices
+      ? getImageSummaryPriceEntries(props.model.image_per_size_prices)
+      : []
+    priceSummary = (
+      <>
+        {imageEntries.map((entry) => (
             <span
               key={entry.key}
               className='text-muted-foreground whitespace-nowrap'
             >
               {entry.label}{' '}
               <span className='text-foreground font-mono font-semibold'>
                 {formatUsdUnitPrice(
                   entry.value,
                   showRechargePrice,
                   priceRate,
                   usdExchangeRate
                 )}
               </span>
               /{t('image')}
             </span>
           )
         )}
      </>
    )
  } else if (isVideoPerSecond) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isImagePerSize && props.model.image_per_size_prices) {
priceSummary = (
<>
{getImageSummaryPriceEntries(props.model.image_per_size_prices).map(
(entry) => (
<span
key={entry.key}
className='text-muted-foreground whitespace-nowrap'
>
{entry.label}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatUsdUnitPrice(
entry.value,
showRechargePrice,
priceRate,
usdExchangeRate
)}
</span>
/{t('image')}
</span>
)
)}
</>
)
} else if (isVideoPerSecond) {
const videoEntries = getOrderedVideoPriceEntries(
getVideoPriceMatrix(props.model)
)
priceSummary = (
<>
{videoEntries.map(([resolution, price]) => (
<span
key={resolution}
className='text-muted-foreground whitespace-nowrap'
>
{resolution}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatUsdUnitPrice(
price,
showRechargePrice,
priceRate,
usdExchangeRate
)}
</span>
/{t('sec')}
</span>
))}
</>
)
} else if (dynamicSummary) {
if (isImagePerSize) {
const imageEntries = props.model.image_per_size_prices
? getImageSummaryPriceEntries(props.model.image_per_size_prices)
: []
priceSummary = (
<>
{imageEntries.map((entry) => (
<span
key={entry.key}
className='text-muted-foreground whitespace-nowrap'
>
{entry.label}{' '}
<span className='text-foreground font-mono font-semibold'>
{formatUsdUnitPrice(
entry.value,
showRechargePrice,
priceRate,
usdExchangeRate
)}
</span>
/{t('image')}
</span>
))}
</>
)
} else if (isVideoPerSecond) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/pricing/components/model-card.tsx` around lines 109
- 158, Update the pricing summary branching around isImagePerSize so
image-per-size models always use the image pricing path, even when
image_per_size_prices is null or missing. Handle the absent price list by
rendering an empty entry list, matching the behavior used by model-details.tsx
and pricing-columns.tsx, while preserving the existing video and other pricing
branches.

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