Skip to content

feat(seedance): 视频按分辨率×含视频精确计费 + 响应分辨率结算 + 退款对账展示 - #5387

Open
iceseaboy wants to merge 7 commits into
QuantumNous:mainfrom
iceseaboy:feat/seedance-resolution-billing
Open

feat(seedance): 视频按分辨率×含视频精确计费 + 响应分辨率结算 + 退款对账展示#5387
iceseaboy wants to merge 7 commits into
QuantumNous:mainfrom
iceseaboy:feat/seedance-resolution-billing

Conversation

@iceseaboy

@iceseaboy iceseaboy commented Jun 8, 2026

Copy link
Copy Markdown

背景

doubao-seedance-2-0-260128 / doubao-seedance-2-0-fast-260128 的上游(火山方舟)按 token 单价计费,且单价随两个维度变化:

480p/720p 1080p
输入不含视频 46 51
输入包含视频 28 31

(2.0 fast 不支持 1080p:不含视频 37 / 含视频 22)

现状 videoInputRatioMap 只处理「是否含视频」一个维度,输出分辨率(480/720 vs 1080)无法区分,1080p 会按低分辨率单价少收约 10%。

改动

1. 二维精确计费

  • constants.go:一维 videoInputRatioMap → 二维 videoPricingMap(输出分辨率档 × 是否含视频),按上游实际单价建表。
  • 新增 GetVideoBillingRatio(model, resolution, hasVideo),返回 实际单价 / base 的精确比率,乘到基础额度上。
  • base = 低分辨率不含视频价(pro 46 / fast 37),管理员配置为 ModelRatio(按量计费),其余档位由代码自动折算。

2. 按响应分辨率结算(修正请求侧旁路)

  • 预扣阶段按请求 metadata.resolution 估算;结算阶段改用上游响应实际返回的 resolution(权威值)重算,二者不一致时按响应档差额补扣/退还。
  • 解决提示词尾部 --rs 1080p 这类弱校验写法绕过请求侧解析、导致 1080p 少收的问题——无论请求侧如何,最终都以上游实际产出的分辨率为准。
  • adaptor.goParseTaskResult 回填 taskResult.Resolution;新增 AdjustBillingOnComplete,用响应分辨率重算冻结的 OtherRatios 后交由 token 重算结算。

3. 退款日志对账与计费过程展示

  • 异步任务退款日志补全金额:显示退款金额、预扣 → 实际扣费,可对账(原先退款无金额无法对账)。
  • 展开区新增「计费过程」:分辨率 / 单价 / token / 单价 × token = 实扣;单价按分辨率档显示实际价(720p ¥46、1080p ¥51),不再统一显示基准价。
  • default 与 classic 两套主题同步实现,并补充相关 i18n。

计费效果

token 数随分辨率天然增多的部分由 total_tokens 承担,本改动只修正「每-token 单价档位差」,不双重计费:total_tokens(1080) × (46/1M) × (51/46) = total_tokens × 51/1M,精确等于上游价目。

模型 分辨率 含视频 最终单价
pro 720p/默认 46
pro 480/720 28
pro 1080p 51
pro 1080p 31
fast 任意 37
fast 任意 22

说明

  • 默认分辨率:请求未传 resolution 时预扣按 720p 档(火山默认 720p)估算,最终以响应分辨率结算
  • fast 无 1080p:传 1080p 会退化为低分辨率档(上游本就拒绝该请求)。

Summary by CodeRabbit

  • Bug Fixes

    • Video billing now accounts for both requested/output resolution and whether the input includes video, producing more accurate charges and updating billing details at task completion.
  • New Features

    • Usage log refund entries always show a structured refund section with Refund Amount, optional Pre‑consumed and Actual Cost, Task ID, Reason, an extra muted refund segment, and an expandable “billing process” detail.
    • Refund details now include reported resolution and token totals when available.
  • Documentation

    • Added English and Chinese translations for "Refund Amount", "Actual Cost", and related billing labels.

- doubao 视频计费改为「输出分辨率档 × 是否含视频」二维定价
- 新增 GetVideoBillingRatio,按 实际单价/base 返回精确折扣比率
- EstimateBilling 增加 metadata.resolution 解析,1080p 自动加价
- base 为低分辨率不含视频价(pro 46 / fast 37),管理员配为 ModelRatio
- seedance 2.0 fast 无 1080p,退化为低分辨率档
@coderabbitai

coderabbitai Bot commented Jun 8, 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

Refactors Doubao video billing to use a resolution- and video-aware pricing model and integrates it into TaskAdaptor.EstimateBilling; expands usage-logs types, adds i18n keys, shows refund amounts in list segments, and always renders structured refund details in the dialog.

Changes

Video Billing Pricing Refactor

Layer / File(s) Summary
Video pricing model and lookup function
relay/channel/task/doubao/constants.go
Introduce videoPricing and videoPricingMap with base and per-variant prices; add GetVideoBillingRatio(modelName, resolution, hasVideo) returning multiplier relative to base.
TaskAdaptor: resolution-aware billing and result handling
relay/channel/task/doubao/adaptor.go
Update TaskAdaptor.EstimateBilling to extract resolution from request metadata, detect video input, call GetVideoBillingRatio, and return a video_input multiplier when applicable; add resolutionFromMetadata helper and propagate upstream resTask.Resolution to enable completion-time recomputation.
Billing context, persistence, and API exposure
constant/context_key.go, controller/relay.go, model/task.go, relay/common/relay_info.go, service/task_billing.go
Add ContextKeyTaskVideoHasInput; persist HasVideoInput, Resolution, TotalTokens in TaskBillingContext; read the flag in RelayTask; expose resolution and total_tokens in task billing Other and TaskInfo.

Usage-logs UI, types and i18n

Layer / File(s) Summary
Types and translations
web/default/src/features/usage-logs/types.ts, web/default/src/i18n/locales/en.json, web/default/src/i18n/locales/zh.json, web/classic/src/i18n/locales/en.json, web/classic/src/i18n/locales/zh-CN.json
Add optional pre_consumed_quota and actual_quota to LogOtherData; add translation entries for Refund Amount, Actual Cost, and billing labels used in UI.
Refund display and render helpers
web/default/src/features/usage-logs/components/columns/common-logs-columns.tsx, web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx, web/classic/src/components/table/usage-logs/UsageLogsColumnDefs.jsx, web/classic/src/helpers/render.jsx, web/classic/src/hooks/usage-logs/useUsageLogsData.jsx
Show refund amount as a muted second segment in list details for log.type === 6; always render refund-details section when isRefund is true and include refund amount, optional pre-consumed, actual cost, task id, and reason. Add renderTaskRefundProcess and wire it into usage-log detail rendering.

Sequence Diagram(s)

sequenceDiagram
  participant TaskAdaptor
  participant GetVideoBillingRatio
  participant videoPricingMap
  TaskAdaptor->>GetVideoBillingRatio: call(modelName, resolution, hasVideo)
  GetVideoBillingRatio->>videoPricingMap: lookup model pricing
  videoPricingMap-->>GetVideoBillingRatio: return selected variant price
  GetVideoBillingRatio-->>TaskAdaptor: return multiplier (selectedPrice/base)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • Calcium-Ion
  • xyfacai

Poem

🐰 I hop through pixels, counting frames so bright,

Ratios tucked under moonlit byte,
Refund lines bloom where quotas used to hide,
Resolution whispers guide the billing tide,
A carrot nod — the rabbit calls it right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% 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 The PR title clearly summarizes the main changes: video billing by resolution×video-presence, response resolution settlement, and refund reconciliation display. It directly reflects the primary objectives of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 and usage tips.

@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)
relay/channel/task/doubao/constants.go (1)

42-63: 💤 Low value

Case-insensitive resolution comparison may be safer.

The comparison resolution == "1080p" (line 50) is case-sensitive. If the metadata contains "1080P" or "1080" without the p suffix, the user would be billed at low-res rates even when 1080p output is requested.

Consider normalizing the resolution input or using a case-insensitive comparison if the upstream/request format isn't strictly controlled.

♻️ Optional: normalize resolution before comparison
 func GetVideoBillingRatio(modelName, resolution string, hasVideo bool) (float64, bool) {
 	p, ok := videoPricingMap[modelName]
 	if !ok || p.base <= 0 {
 		return 0, false
 	}
-	is1080 := p.supports1080 && resolution == "1080p"
+	is1080 := p.supports1080 && strings.EqualFold(resolution, "1080p")
 	var price float64
🤖 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/channel/task/doubao/constants.go` around lines 42 - 63,
GetVideoBillingRatio currently checks resolution == "1080p" case-sensitively and
will miss variants like "1080P" or "1080"; normalize the resolution before
checking (e.g. use strings.ToLower and strip a trailing "p" or use
strings.EqualFold and accept both "1080" and "1080p") so is1080 becomes true for
common variants; update the is1080 logic in GetVideoBillingRatio and add the
necessary import (strings) if not present.
🤖 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/channel/task/doubao/adaptor.go`:
- Around line 135-147: The EstimateBilling method in TaskAdaptor currently calls
GetVideoBillingRatio with info.OriginModelName which can miss entries when
ModelMappedHelper rewrites only info.UpstreamModelName; update EstimateBilling
to call GetVideoBillingRatio(info.UpstreamModelName, resolution, hasVideo)
instead of OriginModelName (keep resolutionFromMetadata, hasVideoInMetadata
logic), so videoPricingMap lookups (e.g. doubao-seedance-2-0-260128) use the
actual upstream model id; reference symbols: TaskAdaptor.EstimateBilling,
GetVideoBillingRatio, info.UpstreamModelName, info.OriginModelName,
ModelMappedHelper, RelayModeResponsesCompact, videoPricingMap.

---

Nitpick comments:
In `@relay/channel/task/doubao/constants.go`:
- Around line 42-63: GetVideoBillingRatio currently checks resolution == "1080p"
case-sensitively and will miss variants like "1080P" or "1080"; normalize the
resolution before checking (e.g. use strings.ToLower and strip a trailing "p" or
use strings.EqualFold and accept both "1080" and "1080p") so is1080 becomes true
for common variants; update the is1080 logic in GetVideoBillingRatio and add the
necessary import (strings) if not present.
🪄 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: 0773ba12-421d-4b45-b25e-6c3f2738e2ef

📥 Commits

Reviewing files that changed from the base of the PR and between d2576dd and 89c26d2.

📒 Files selected for processing (2)
  • relay/channel/task/doubao/adaptor.go
  • relay/channel/task/doubao/constants.go

Comment on lines +135 to 147
// EstimateBilling 根据请求的输出分辨率与是否含视频输入,返回相对基准价的计费 OtherRatio。
func (a *TaskAdaptor) EstimateBilling(c *gin.Context, info *relaycommon.RelayInfo) map[string]float64 {
req, err := relaycommon.GetTaskRequest(c)
if err != nil {
return nil
}
if hasVideoInMetadata(req.Metadata) {
if ratio, ok := GetVideoInputRatio(info.OriginModelName); ok {
return map[string]float64{"video_input": ratio}
}
resolution := resolutionFromMetadata(req.Metadata)
hasVideo := hasVideoInMetadata(req.Metadata)
if ratio, ok := GetVideoBillingRatio(info.OriginModelName, resolution, hasVideo); ok && ratio != 1.0 {
return map[string]float64{"video_input": ratio}
}
return nil
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if other task adaptors use OriginModelName or UpstreamModelName for billing lookups
rg -n "EstimateBilling|OriginModelName|UpstreamModelName" relay/channel/task/ -A2 -B2 --type go | head -100

Repository: QuantumNous/new-api

Length of output: 6858


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate doubao EstimateBilling and related pricing map usage
rg -n "EstimateBilling|GetVideoBillingRatio|videoPricingMap|OriginModelName|UpstreamModelName" relay/channel/task/doubao/adaptor.go -n

# Find where GetVideoBillingRatio and videoPricingMap are defined
rg -n "func GetVideoBillingRatio|videoPricingMap" relay/channel/task/ -S

Repository: QuantumNous/new-api

Length of output: 827


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "EstimateBilling|GetVideoBillingRatio|videoPricingMap|OriginModelName|UpstreamModelName" relay/channel/task/doubao/adaptor.go

rg -n "GetVideoBillingRatio|videoPricingMap" relay/channel/task/ -S

Repository: QuantumNous/new-api

Length of output: 1068


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate doubao EstimateBilling and related pricing map usage
rg -n "EstimateBilling|GetVideoBillingRatio|videoPricingMap|OriginModelName|UpstreamModelName" relay/channel/task/doubao/adaptor.go

# Find where GetVideoBillingRatio and videoPricingMap are defined
rg -n "func GetVideoBillingRatio|videoPricingMap" relay/channel/task/ -S

Repository: QuantumNous/new-api

Length of output: 827


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show RelayInfo struct + comments
rg -n "type RelayInfo struct" -S . | head -20

# Dump the RelayInfo definition
file=$(rg -n "type RelayInfo struct" -S . | head -1 | cut -d: -f1)
echo "---- $file ----"
sed -n '1,200p' "$file"

# Find where OriginModelName and UpstreamModelName are assigned
rg -n "OriginModelName\s*=" -S relay | head -50
rg -n "UpstreamModelName\s*=" -S relay | head -50

# Inspect doubao pricing map + helper
sed -n '1,120p' relay/channel/task/doubao/constants.go
sed -n '120,220p' relay/channel/task/doubao/constants.go

Repository: QuantumNous/new-api

Length of output: 11561


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1,260p' relay/relay_task.go
sed -n '1,140p' relay/helper/model_mapped.go

# Also inspect the task relay where info.Action/metadata might affect origin/upstream model names
rg -n "IsModelMapped|OriginModelName =|UpstreamModelName =" relay/relay_task.go relay/helper/model_mapped.go relay/responses_handler.go relay/gemini_handler.go -S

Repository: QuantumNous/new-api

Length of output: 12266


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find where task submission sets/assigns OriginModelName
rg -n "OriginModelName\s*=" -S relay | rg "task|Task" -n || true
rg -n "OriginModelName\s*=" -S . | rg -n "task" | head -50

# Inspect ModelPriceHelperPerCall to see whether it uses OriginModelName or UpstreamModelName
rg -n "func ModelPriceHelperPerCall" -S relay/common relay/helper relay | head -20
file=$(rg -n "func ModelPriceHelperPerCall" -S relay | head -1 | cut -d: -f1)
echo "---- $file ----"
sed -n '1,240p' "$file"

Repository: QuantumNous/new-api

Length of output: 9087


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '1,260p' relay/channel/task/doubao/adaptor.go
rg -n "OriginModelName|UpstreamModelName" relay/channel/task/doubao/adaptor.go

Repository: QuantumNous/new-api

Length of output: 8382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "ValidateBasicTaskRequest" -S relay/relay/common relay | head -50
file=$(rg -n "func ValidateBasicTaskRequest" -S relay | head -1 | cut -d: -f1)
echo "---- $file ----"
sed -n '1,220p' "$file"

# Also find where RelayMode is set for task requests
rg -n "RelayModeResponsesCompact|RelayMode\s*=" -S relay | head -50
rg -n "ContextKey.*RelayMode|SetString\\(\"relay_mode\"\\)|ContextKey.*Relay" -S . | head -50

Repository: QuantumNous/new-api

Length of output: 1153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="relay/common/relay_utils.go"
# confirm function exists
rg -n "func ValidateBasicTaskRequest" "$file"

start=$(rg -n "func ValidateBasicTaskRequest" "$file" | head -1 | cut -d: -f1)
# print surrounding section
sed -n "${start},$((start+260))p" "$file"

Repository: QuantumNous/new-api

Length of output: 1010


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="relay/common/relay_utils.go"
rg -n "func storeTaskRequest" "$file"

start=$(rg -n "func storeTaskRequest" "$file" | head -1 | cut -d: -f1)
sed -n "${start},$((start+260))p" "$file"

rg -n "OriginModelName|UpstreamModelName|model_mapping|RelayModeResponsesCompact" "$file" | head -80

Repository: QuantumNous/new-api

Length of output: 4730


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "GetTaskRequest\\(c\\).*OriginModelName|OriginModelName.*GetTaskRequest\\(c\\)" -S relay | head -50
rg -n "OriginModelName\\s*=\\s*.*req\\.Model|OriginModelName\\s*=\\s*.*Model" -S relay | head -80
rg -n "task_request" relay | rg -n "OriginModelName|UpstreamModelName" || true

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "RelayTaskSubmit\\(" -S . | head -50

# Find controllers/handlers likely under router or relay for task endpoints
rg -n "task.*submit|/v1/videos|/v1/audio|generations/tasks|videos/.*remix" -S router relay | head -100

# Find direct assignments to OriginModelName outside relay/relay_task.go
rg -n "OriginModelName\\s*=" -S . | head -80

Repository: QuantumNous/new-api

Length of output: 6942


🏁 Script executed:

#!/bin/bash
set -euo pipefail
sed -n '480,620p' controller/relay.go
rg -n "relayInfo\\." controller/relay.go | head -80

Repository: QuantumNous/new-api

Length of output: 6511


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "model_mapping" -S . | head -80
rg -n "ContextKey.*model_mapping|\"model_mapping\"" -S . | head -80
rg -n "GetString\\(\"model_mapping\"\\)" -S . | head -20

Repository: QuantumNous/new-api

Length of output: 12373


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect relay mode constants + logic
sed -n '1,220p' relay/constant/relay_mode.go
rg -n "RelayModeResponsesCompact" -S relay | head -50

# Find GenRelayInfo and how it sets RelayMode
rg -n "func GenRelayInfo" -S relay | head -20
file=$(rg -n "func GenRelayInfo" -S relay | head -1 | cut -d: -f1)
echo "---- $file ----"
start=$(rg -n "func GenRelayInfo" -S relay | head -1 | cut -d: -f1)
sed -n "${start},$((start+260))p" "$file"

Repository: QuantumNous/new-api

Length of output: 7986


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="relay/common/relay_info.go"
# show GenRelayInfo branches around where it sets RelayMode for different relay formats
rg -n "func GenRelayInfo\\(" "$file"
start=$(rg -n "func GenRelayInfo\\(" "$file" | head -1 | cut -d: -f1)
sed -n "${start},$((start+260))p" "$file"

# show the task relay format assignment if present
rg -n "RelayFormatTask" "$file" | head -50

Repository: QuantumNous/new-api

Length of output: 8179


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '130,260p' model/task.go
rg -n "UpstreamModelName" model/task.go

Repository: QuantumNous/new-api

Length of output: 3688


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "SetupContextForSelectedChannel" -S . | head -50
file=$(rg -n "func SetupContextForSelectedChannel" -S . | head -1 | cut -d: -f1)
echo "---- $file ----"
sed -n '1,220p' "$file"

rg -n "ContextKeyChannelModelMapping|model_mapping" -S middleware relay controller | head -80

Repository: QuantumNous/new-api

Length of output: 9298


Switch doubao EstimateBilling to use info.UpstreamModelName (model_mapping can break videoPricingMap lookup)

relay/channel/task/doubao/adaptor.go calls GetVideoBillingRatio(info.OriginModelName, ...) (line ~143). For task relays, RelayTaskSubmit sets info.OriginModelName/info.UpstreamModelName to the same value before ModelMappedHelper, but ModelMappedHelper only updates info.UpstreamModelName under model_mapping (it rewrites OriginModelName only for RelayModeResponsesCompact). Meanwhile videoPricingMap in relay/channel/task/doubao/constants.go is hardcoded to upstream IDs like doubao-seedance-2-0-260128. So if model_mapping redirects a request from an alias to these upstream IDs, the ratio lookup can miss and the video_input OtherRatio won’t be applied.

Use info.UpstreamModelName for GetVideoBillingRatio (matching the Gemini adaptor pattern) or explicitly require that model_mapping source keys match the hardcoded videoPricingMap keys.

🤖 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/channel/task/doubao/adaptor.go` around lines 135 - 147, The
EstimateBilling method in TaskAdaptor currently calls GetVideoBillingRatio with
info.OriginModelName which can miss entries when ModelMappedHelper rewrites only
info.UpstreamModelName; update EstimateBilling to call
GetVideoBillingRatio(info.UpstreamModelName, resolution, hasVideo) instead of
OriginModelName (keep resolutionFromMetadata, hasVideoInMetadata logic), so
videoPricingMap lookups (e.g. doubao-seedance-2-0-260128) use the actual
upstream model id; reference symbols: TaskAdaptor.EstimateBilling,
GetVideoBillingRatio, info.UpstreamModelName, info.OriginModelName,
ModelMappedHelper, RelayModeResponsesCompact, videoPricingMap.

iceseaboy added 5 commits June 9, 2026 00:41
- 列表「详情」退款行补充退款金额
- 详情弹窗退款区新增 退款金额/预扣费/实际扣费/原因,便于对账
- LogOtherData 补 pre_consumed_quota / actual_quota 字段
- i18n 补「退款金额/实际扣费」
- classic「详情」列退款行补充 退款金额 + 预扣→实际扣费对照
- i18n 补「实际扣费」
- 退款行展开面板新增「计费过程」:退款金额 + 预扣→实际扣费 + token重算明细
- 新增 renderTaskRefundProcess
- 结算改用响应真实 resolution(AdjustBillingOnComplete),规避 --rs 旁路等请求/出片不一致
- TaskInfo/TaskBillingContext 增加 resolution/total_tokens/has_video_input
- 退款日志「计费过程」展示 分辨率/单价/token/折扣 计算公式
…ution-billing

# Conflicts:
#	web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx

@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

♻️ Duplicate comments (1)
relay/channel/task/doubao/adaptor.go (1)

143-146: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use info.UpstreamModelName for pricing lookup to support model mapping.

This is consistent with the past review finding. videoPricingMap keys are hardcoded upstream IDs (e.g., doubao-seedance-2-0-260128). When model_mapping redirects an alias to these upstream IDs, info.OriginModelName retains the alias while only info.UpstreamModelName is updated, causing the lookup to miss.

Proposed fix
-	ratio, ok := GetVideoBillingRatio(info.OriginModelName, resolution, hasVideo)
+	ratio, ok := GetVideoBillingRatio(info.UpstreamModelName, resolution, hasVideo)
🤖 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/channel/task/doubao/adaptor.go` around lines 143 - 146, The lookup is
using info.OriginModelName which can be an alias and misses entries in
videoPricingMap; update the call to GetVideoBillingRatio to use
info.UpstreamModelName (falling back to OriginModelName if UpstreamModelName is
empty) so pricing uses the mapped upstream ID; change the invocation around the
ratio, ok assignment in the block where GetVideoBillingRatio is called
(reference: GetVideoBillingRatio, info.OriginModelName, info.UpstreamModelName)
and ensure behavior remains the same if no upstream name is present.
🤖 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/channel/task/doubao/adaptor.go`:
- Around line 174-177: TaskBillingContext currently only snapshots
relayInfo.OriginModelName which can be the source/alias name; add a new
UpstreamModelName field to TaskBillingContext and ensure ModelMappedHelper
(which sets relayInfo.UpstreamModelName) is copied into that field when creating
the billing snapshot (leave OriginModelName for display). Then modify
AdjustBillingOnComplete to call GetVideoBillingRatio using bc.UpstreamModelName
(falling back to bc.OriginModelName if UpstreamModelName is empty) so
videoPricingMap lookups use the mapped target model; update any constructors or
usages that build TaskBillingContext to populate the new field.

In `@web/classic/src/helpers/render.jsx`:
- Around line 1630-1633: The code currently sets discount from other.video_input
which no longer exists; update the logic that assigns discount (the variable
currently set on the line with Number(other?.video_input) || 1) to use the
persisted refund metadata: check other?.has_video_input (boolean) and, when
true, derive the multiplier from other?.resolution (or a small helper like
computeResolutionMultiplier/resolutionMultiplier) otherwise default to 1; leave
modelRatio, groupRatio and tokens assignments as-is but replace the discount
assignment with this has_video_input + resolution-based computation so the
“分辨率折扣/计算” uses the new contract.

---

Duplicate comments:
In `@relay/channel/task/doubao/adaptor.go`:
- Around line 143-146: The lookup is using info.OriginModelName which can be an
alias and misses entries in videoPricingMap; update the call to
GetVideoBillingRatio to use info.UpstreamModelName (falling back to
OriginModelName if UpstreamModelName is empty) so pricing uses the mapped
upstream ID; change the invocation around the ratio, ok assignment in the block
where GetVideoBillingRatio is called (reference: GetVideoBillingRatio,
info.OriginModelName, info.UpstreamModelName) and ensure behavior remains the
same if no upstream name is present.
🪄 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: 51997b7b-35eb-4b25-af2a-6d807aae74af

📥 Commits

Reviewing files that changed from the base of the PR and between 59de197 and a44508c.

📒 Files selected for processing (12)
  • constant/context_key.go
  • controller/relay.go
  • model/task.go
  • relay/channel/task/doubao/adaptor.go
  • relay/common/relay_info.go
  • service/task_billing.go
  • web/classic/src/helpers/render.jsx
  • web/classic/src/i18n/locales/en.json
  • web/classic/src/i18n/locales/zh-CN.json
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/zh.json
💤 Files with no reviewable changes (3)
  • web/default/src/i18n/locales/zh.json
  • web/default/src/i18n/locales/en.json
  • web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
✅ Files skipped from review due to trivial changes (3)
  • constant/context_key.go
  • web/classic/src/i18n/locales/zh-CN.json
  • web/classic/src/i18n/locales/en.json

Comment on lines +174 to +177
ratio, ok := GetVideoBillingRatio(bc.OriginModelName, actualResolution, bc.HasVideoInput)
if !ok {
return 0
}

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.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify how BillingContext.OriginModelName is set and whether UpstreamModelName is available
rg -n "BillingContext\s*=" -A5 controller/relay.go
rg -n "UpstreamModelName" model/task.go | head -20

Repository: QuantumNous/new-api

Length of output: 600


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find definition/struct of relayInfo and BillingContext
rg -n "type .*RelayInfo|struct .*RelayInfo|RelayInfo" controller/relay.go model -S || true
rg -n "type .*TaskBillingContext|TaskBillingContext" -S model controller relay || true

# 2) Trace where OriginModelName is set on relayInfo
rg -n "OriginModelName" controller/relay.go controller -S

# 3) Look for model_mapping usage
rg -n "model_mapping" -S . | head -50

# 4) Find GetVideoBillingRatio and its expectations (what map key it uses)
rg -n "GetVideoBillingRatio" -S .

Repository: QuantumNous/new-api

Length of output: 12182


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show relevant context around OriginModelName assignment(s) in controller/relay.go
rg -n "OriginModelName" controller/relay.go -n -C 2

# Show BillingContext definition
rg -n "type TaskBillingContext" -S model/task.go

# Show relayInfo struct around OriginModelName
rg -n "OriginModelName" controller/relay.go -C 6

# Trace where relayInfo.OriginModelName is computed (look for "relayInfo" assignments)
rg -n "relayInfo\." controller/relay.go -S | head -80

# Find where UpstreamModelName is filled (context)
rg -n "UpstreamModelName" controller/relay.go -C 4

# Show GetVideoBillingRatio implementation
rg -n "func GetVideoBillingRatio" -S .

Repository: QuantumNous/new-api

Length of output: 6822


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect TaskBillingContext fields (OriginModelName vs UpstreamModelName) in model/task.go
sed -n '90,220p' model/task.go | nl -ba

# 2) Inspect GetVideoBillingRatio implementation and its pricing map keys
rg -n "func GetVideoBillingRatio" -n relay/channel/task/doubao/constants.go
sed -n '1,120p' relay/channel/task/doubao/constants.go | nl -ba

# 3) Find how relayInfo.OriginModelName and relayInfo.UpstreamModelName are populated
#    Search for struct field assignments in controller/relay.go and relaycommon (if exists).
rg -n "OriginModelName\s*:" -S . | head -50
rg -n "UpstreamModelName\s*:" -S . | head -50

# 4) Inspect relaycommon.RelayInfo definition to understand intended semantics
rg -n "type RelayInfo" -S . | head -50
# Try to print the most relevant RelayInfo struct definition (limit noise)
fd "relay.*info*.go" -a . 2>/dev/null | head -20

Repository: QuantumNous/new-api

Length of output: 106


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect TaskBillingContext fields
cat -n model/task.go | sed -n '90,190p'

# 2) Inspect GetVideoBillingRatio implementation and its pricing map keys
cat -n relay/channel/task/doubao/constants.go | sed -n '1,120p'

# 3) Locate and inspect relaycommon.RelayInfo definition (so we know semantics of OriginModelName vs UpstreamModelName)
#    First find the file that defines "type RelayInfo" near "relaycommon"
rg -n "type RelayInfo" -S . | head -50
# show RelayInfo definition around the relevant one
# (best-effort: locate relaycommon package file by matching relaycommon.RelayInfo usage and definition)
relay_info_file="$(rg -n --files-with-matches "type RelayInfo" -S . | head -1 || true)"
echo "relay_info_file=$relay_info_file"
if [ -n "$relay_info_file" ] && [ -f "$relay_info_file" ]; then
  cat -n "$relay_info_file" | sed -n '1,120p'
fi

# 4) Inspect controller/relay.go around where TaskBillingContext is populated
cat -n controller/relay.go | sed -n '550,620p'

# 5) Inspect where OriginModelName and UpstreamModelName are assigned into relayInfo, including any model_mapping logic
rg -n "OriginModelName\s*=" -S controller/relay.go
rg -n "UpstreamModelName\s*=" -S controller/relay.go
rg -n "OriginModelName:\s*relayInfo\.OriginModelName" -S controller/relay.go
rg -n "UpstreamModelName:\s*relayInfo\.UpstreamModelName" -S controller/relay.go

Repository: QuantumNous/new-api

Length of output: 13302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Find UpstreamModelName in relay/common/relay_info.go
rg -n "UpstreamModelName" relay/common/relay_info.go

# 2) Print RelayInfo struct portion containing OriginModelName/UpstreamModelName
#    (show a wider window around those lines)
line_origin="$(rg -n "OriginModelName" relay/common/relay_info.go | head -1 | cut -d: -f1)"
line_upstream="$(rg -n "UpstreamModelName" relay/common/relay_info.go | head -1 | cut -d: -f1 || true)"
echo "line_origin=$line_origin line_upstream=$line_upstream"
if [ -n "$line_origin" ]; then sed -n "$((line_origin-15)),$((line_origin+30))p" relay/common/relay_info.go; fi
if [ -n "${line_upstream:-}" ]; then sed -n "$((line_upstream-15)),$((line_upstream+30))p" relay/common/relay_info.go; fi

# 3) Locate GenRelayInfo and inspect where OriginModelName / UpstreamModelName are set
gen_file="$(rg -n "func GenRelayInfo" -S relay | head -1 | cut -d: -f1 || true)"
echo "gen_file=$gen_file"
if [ -n "$gen_file" ]; then
  # show around definition
  sed -n '1,220p' "$gen_file"
  rg -n "OriginModelName|UpstreamModelName" "$gen_file"
  # show nearby assignments
  rg -n "OriginModelName\s*=" "$gen_file" -C 3 || true
  rg -n "UpstreamModelName\s*=" "$gen_file" -C 3 || true
fi

# 4) Search for model_mapping logic inside GenRelayInfo / relay/common
rg -n "model_mapping" -S relay/common relay | head -80

# 5) Check how pricing/adaptor uses OriginModelName vs upstream in doubao adaptor
sed -n '120,210p' relay/channel/task/doubao/adaptor.go | cat -n

Repository: QuantumNous/new-api

Length of output: 15663


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show RelayInfo field assignments for OriginModelName
rg -n "OriginModelName\s*:" -S relay controller | head -50
rg -n "OriginModelName\s*=" -S relay controller | head -50

# Inspect where model_mapping is applied
rg -n "model_mapping" -S relay | head -80

# Inspect model mapping helper
sed -n '1,220p' relay/helper/model_mapped.go | cat -n

# Inspect relay_task where model_mapping_failed exists
sed -n '120,260p' relay/relay_task.go | cat -n

# Find GenRelayInfo and inspect it (likely in relay/common or relaycommon package)
gen_file="$(rg -n "func GenRelayInfo" -S relay controller common | head -1 | cut -d: -f1)"
echo "GenRelayInfo in: $gen_file"
if [ -n "$gen_file" ]; then sed -n '1,260p' "$gen_file" | cat -n; fi

Repository: QuantumNous/new-api

Length of output: 21157


Persist the upstream model name for AdjustBillingOnComplete ratio lookup

TaskBillingContext.OriginModelName is snapshotted from relayInfo.OriginModelName in controller/relay.go, and AdjustBillingOnComplete then calls GetVideoBillingRatio(bc.OriginModelName, ...). With model_mapping, ModelMappedHelper sets info.UpstreamModelName to the mapped target model, while info.OriginModelName generally remains the original/source name (except the responses-compact path). Since GetVideoBillingRatio indexes videoPricingMap by the provided model string, completion billing can miss the correct entry when the alias/source name doesn’t match the map keys.

Root fix: store the mapped upstream model name in the billing snapshot (likely by adding an UpstreamModelName field to TaskBillingContext, keeping OriginModelName for display/logging) and use that upstream value for GetVideoBillingRatio inside AdjustBillingOnComplete.

🤖 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/channel/task/doubao/adaptor.go` around lines 174 - 177,
TaskBillingContext currently only snapshots relayInfo.OriginModelName which can
be the source/alias name; add a new UpstreamModelName field to
TaskBillingContext and ensure ModelMappedHelper (which sets
relayInfo.UpstreamModelName) is copied into that field when creating the billing
snapshot (leave OriginModelName for display). Then modify
AdjustBillingOnComplete to call GetVideoBillingRatio using bc.UpstreamModelName
(falling back to bc.OriginModelName if UpstreamModelName is empty) so
videoPricingMap lookups use the mapped target model; update any constructors or
usages that build TaskBillingContext to populate the new field.

Comment on lines +1630 to +1633
const modelRatio = Number(other?.model_ratio) || 0;
const groupRatio = Number(other?.group_ratio) || 1;
const discount = Number(other?.video_input) || 1; // 分辨率档乘子(otherMultiplier)
const tokens = Number(other?.total_tokens) || 0;

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use the persisted refund metadata key; video_input breaks the new contract.

Line 1632 derives the multiplier from other.video_input, but this PR’s backend contract persists has_video_input (plus resolution/total_tokens). With a boolean field, Number(other?.video_input) || 1 collapses to 1, so the “分辨率折扣/计算” line is incorrect or always neutral.

Suggested fix
-  const discount = Number(other?.video_input) || 1; // 分辨率档乘子(otherMultiplier)
+  // Use backend-provided multiplier field if present; keep has_video_input as a separate boolean context field.
+  const discount = Number(other?.video_ratio_multiplier ?? other?.resolution_ratio ?? 1);
+  const hasVideoInput = Boolean(other?.has_video_input);
📝 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
const modelRatio = Number(other?.model_ratio) || 0;
const groupRatio = Number(other?.group_ratio) || 1;
const discount = Number(other?.video_input) || 1; // 分辨率档乘子(otherMultiplier)
const tokens = Number(other?.total_tokens) || 0;
const modelRatio = Number(other?.model_ratio) || 0;
const groupRatio = Number(other?.group_ratio) || 1;
// Use backend-provided multiplier field if present; keep has_video_input as a separate boolean context field.
const discount = Number(other?.video_ratio_multiplier ?? other?.resolution_ratio ?? 1);
const hasVideoInput = Boolean(other?.has_video_input);
const tokens = Number(other?.total_tokens) || 0;
🤖 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/classic/src/helpers/render.jsx` around lines 1630 - 1633, The code
currently sets discount from other.video_input which no longer exists; update
the logic that assigns discount (the variable currently set on the line with
Number(other?.video_input) || 1) to use the persisted refund metadata: check
other?.has_video_input (boolean) and, when true, derive the multiplier from
other?.resolution (or a small helper like
computeResolutionMultiplier/resolutionMultiplier) otherwise default to 1; leave
modelRatio, groupRatio and tokens assignments as-is but replace the discount
assignment with this has_video_input + resolution-based computation so the
“分辨率折扣/计算” uses the new contract.

- 单价改为「基准倍率 × 分辨率/视频档乘子」,720p/1080p 各显示 ¥46/¥51
- 去掉单独「分辨率折扣」行,乘子并入单价
- 删除无用的 分辨率折扣 i18n key(zh-CN/en)
@iceseaboy iceseaboy changed the title feat(seedance): 视频按分辨率×含视频精确计费 feat(seedance): 视频按分辨率×含视频精确计费 + 响应分辨率结算 + 退款对账展示 Jun 10, 2026
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