feat(seedance): 视频按分辨率×含视频精确计费 + 响应分辨率结算 + 退款对账展示 - #5387
Conversation
- doubao 视频计费改为「输出分辨率档 × 是否含视频」二维定价 - 新增 GetVideoBillingRatio,按 实际单价/base 返回精确折扣比率 - EstimateBilling 增加 metadata.resolution 解析,1080p 自动加价 - base 为低分辨率不含视频价(pro 46 / fast 37),管理员配为 ModelRatio - seedance 2.0 fast 无 1080p,退化为低分辨率档
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughRefactors 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. ChangesVideo Billing Pricing Refactor
Usage-logs UI, types and i18n
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)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
relay/channel/task/doubao/constants.go (1)
42-63: 💤 Low valueCase-insensitive resolution comparison may be safer.
The comparison
resolution == "1080p"(line 50) is case-sensitive. If the metadata contains"1080P"or"1080"without thepsuffix, 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
📒 Files selected for processing (2)
relay/channel/task/doubao/adaptor.gorelay/channel/task/doubao/constants.go
| // 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 | ||
| } |
There was a problem hiding this comment.
🧩 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 -100Repository: 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/ -SRepository: 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/ -SRepository: 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/ -SRepository: 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.goRepository: 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 -SRepository: 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.goRepository: 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 -50Repository: 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 -80Repository: 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" || trueRepository: 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 -80Repository: 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 -80Repository: 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 -20Repository: 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 -50Repository: 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.goRepository: 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 -80Repository: 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.
- 列表「详情」退款行补充退款金额 - 详情弹窗退款区新增 退款金额/预扣费/实际扣费/原因,便于对账 - 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
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
relay/channel/task/doubao/adaptor.go (1)
143-146:⚠️ Potential issue | 🟠 Major | ⚡ Quick winUse
info.UpstreamModelNamefor pricing lookup to support model mapping.This is consistent with the past review finding.
videoPricingMapkeys are hardcoded upstream IDs (e.g.,doubao-seedance-2-0-260128). Whenmodel_mappingredirects an alias to these upstream IDs,info.OriginModelNameretains the alias while onlyinfo.UpstreamModelNameis 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
📒 Files selected for processing (12)
constant/context_key.gocontroller/relay.gomodel/task.gorelay/channel/task/doubao/adaptor.gorelay/common/relay_info.goservice/task_billing.goweb/classic/src/helpers/render.jsxweb/classic/src/i18n/locales/en.jsonweb/classic/src/i18n/locales/zh-CN.jsonweb/default/src/features/usage-logs/components/dialogs/details-dialog.tsxweb/default/src/i18n/locales/en.jsonweb/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
| ratio, ok := GetVideoBillingRatio(bc.OriginModelName, actualResolution, bc.HasVideoInput) | ||
| if !ok { | ||
| return 0 | ||
| } |
There was a problem hiding this comment.
🧩 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 -20Repository: 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 -20Repository: 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.goRepository: 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 -nRepository: 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; fiRepository: 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.
| 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; |
There was a problem hiding this comment.
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.
| 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)
背景
doubao-seedance-2-0-260128/doubao-seedance-2-0-fast-260128的上游(火山方舟)按 token 单价计费,且单价随两个维度变化:(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.go:ParseTaskResult回填taskResult.Resolution;新增AdjustBillingOnComplete,用响应分辨率重算冻结的OtherRatios后交由 token 重算结算。3. 退款日志对账与计费过程展示
预扣 → 实际扣费,可对账(原先退款无金额无法对账)。单价 × token = 实扣;单价按分辨率档显示实际价(720p ¥46、1080p ¥51),不再统一显示基准价。计费效果
token 数随分辨率天然增多的部分由
total_tokens承担,本改动只修正「每-token 单价档位差」,不双重计费:total_tokens(1080) × (46/1M) × (51/46) = total_tokens × 51/1M,精确等于上游价目。说明
resolution时预扣按 720p 档(火山默认 720p)估算,最终以响应分辨率结算。Summary by CodeRabbit
Bug Fixes
New Features
Documentation