feat(channel): add per-channel billing ratio (渠道倍率) - #5470
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (23)
✅ Files skipped from review due to trivial changes (3)
🚧 Files skipped from review as they are similar to previous changes (19)
WalkthroughAdds a per-channel billing ratio multiplier: stored on Channel, set in middleware context, propagated into RelayInfo/PriceData/TaskBillingContext/BillingSnapshot, applied across price/quota/settlement calculations, and surfaced in frontend channel settings, pricing displays, and usage logs. ChangesChannel Ratio Feature
Sequence Diagram(s)sequenceDiagram
participant Client as API Client
participant Middleware as Middleware:SetupContextForSelectedChannel
participant Init as Relay:InitChannelMeta
participant Helper as Relay:ModelPriceHelper
participant QuotaSvc as Quota Service:calculateAudioQuota
participant Settle as Settlement:ComputeTieredQuotaWithRequest
participant Task as Task Billing:RecalculateTaskQuotaByTokens/LogTaskConsumption
Client->>Middleware: request with selected channel
Middleware->>Middleware: call channel.GetRatio()
Middleware->>Init: set ContextKeyChannelRatio
Init->>Helper: provide ChannelMeta.ChannelRatio
Helper->>QuotaSvc: compute pre-consumed quota using groupRatio × channelRatio
QuotaSvc->>Settle: settle tiered quota using ChannelRatio in BillingSnapshot
Settle->>Task: settled quota used in task billing/recalculation
Task->>Task: log other.{channel_ratio, upstream_quota} when ratio ≠ 1
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
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: 4
🧹 Nitpick comments (2)
web/default/src/features/pricing/lib/price.ts (1)
83-99: 💤 Low valueRemove unused function
getMinGroupRatio.This function is no longer called after being replaced by
getMinEffectiveRatioat lines 204–208 and 310–314. Removing dead code improves maintainability.🤖 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/lib/price.ts` around lines 83 - 99, The function getMinGroupRatio is dead code and should be removed: delete the entire getMinGroupRatio function definition and any tests/usages if found, since getMinEffectiveRatio now replaces it (verify there are no remaining references to getMinGroupRatio before committing). Ensure no other code relies on getMinGroupRatio and run tests to confirm removal is safe.types/price_data.go (1)
41-43: 💤 Low value
ToSetting()omits the newChannelRatiofield.The debug output string includes most pricing fields but not
ChannelRatio. This could make troubleshooting channel-ratio billing issues harder since the debug log atrelay/helper/price.go:166uses this method.🔧 Suggested fix
func (p *PriceData) ToSetting() string { - return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio) + return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, ChannelRatio: %f, UsePrice: %t, CacheCreationRatio: %f, CacheCreation5mRatio: %f, CacheCreation1hRatio: %f, QuotaToPreConsume: %d, ImageRatio: %f, AudioRatio: %f, AudioCompletionRatio: %f", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatioInfo.GroupRatio, p.ChannelRatio, p.UsePrice, p.CacheCreationRatio, p.CacheCreation5mRatio, p.CacheCreation1hRatio, p.QuotaToPreConsume, p.ImageRatio, p.AudioRatio, p.AudioCompletionRatio) }🤖 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 `@types/price_data.go` around lines 41 - 43, The PriceData.ToSetting method is missing the new ChannelRatio field in its debug string; update the fmt.Sprintf format string in ToSetting and its argument list to include ChannelRatio (e.g., add "ChannelRatio: %f" in the format and pass p.ChannelRatio in the corresponding position) so the returned string contains the channel ratio for debugging/troubleshooting.
🤖 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/channels/lib/channel-form.ts`:
- Line 141: Update the channel form schema's field named "ratio" (currently
declared as ratio: z.number().min(0).optional()) so users cannot submit
zero—change the validator to .min(0.01) (e.g., ratio:
z.number().min(0.01).optional()); alternatively, if you prefer to preserve zeros
from the UI, add a .transform on that schema to map 0 -> 1 (or null) before
validation and ensure the UI shows the effective minimum in help text.
In `@web/default/src/features/pricing/lib/price.ts`:
- Line 70: The code currently treats an explicit channel ratio of 0 as valid by
using the nullish coalescing operator, but backend semantics require nil/0 to be
treated as 1.0; update the three places in
web/default/src/features/pricing/lib/price.ts so they coerce falsy 0 to 1: in
getMinEffectiveRatio (line 70) replace the expression using
groupChannelRatios?.[group] ?? 1 with one that treats 0 as 1 (use || 1), and
likewise in formatGroupPrice (line 243) and formatFixedPrice (line 276) replace
(model.group_channel_ratios?.[group] ?? 1) with
(model.group_channel_ratios?.[group] || 1); these changes ensure
getMinEffectiveRatio, formatGroupPrice, and formatFixedPrice all treat nil or 0
as 1.0.
- Around line 55-78: getMinEffectiveRatio currently uses
groupChannelRatios?.[group] ?? 1 which preserves an explicit 0; change the
calculation of cr inside getMinEffectiveRatio so that 0, null, and undefined are
treated as 1.0 (e.g., read the value from groupChannelRatios for the group, and
if the value is null/undefined or === 0, set cr = 1; otherwise use the provided
value) to mirror backend behavior.
In `@web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 220-229: The UI uses t('Channel Ratio') in the DetailsDialog rows
push (the block that sets label: t('Channel Ratio')), but that i18n key is
missing in most locale files; add the "Channel Ratio" key with appropriate
translations to all supported locale JSONs (en, fr, ru, ja, vi — matching the
existing keys structure) or run the project's i18n sync command to propagate the
key (e.g., run the i18n:sync script so all locales include the new key), then
verify the DetailsDialog renders the localized label.
---
Nitpick comments:
In `@types/price_data.go`:
- Around line 41-43: The PriceData.ToSetting method is missing the new
ChannelRatio field in its debug string; update the fmt.Sprintf format string in
ToSetting and its argument list to include ChannelRatio (e.g., add
"ChannelRatio: %f" in the format and pass p.ChannelRatio in the corresponding
position) so the returned string contains the channel ratio for
debugging/troubleshooting.
In `@web/default/src/features/pricing/lib/price.ts`:
- Around line 83-99: The function getMinGroupRatio is dead code and should be
removed: delete the entire getMinGroupRatio function definition and any
tests/usages if found, since getMinEffectiveRatio now replaces it (verify there
are no remaining references to getMinGroupRatio before committing). Ensure no
other code relies on getMinGroupRatio and run tests to confirm removal is safe.
🪄 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: a1749c5b-5d38-4be2-8bbb-f3172692fd3f
📒 Files selected for processing (22)
constant/context_key.gocontroller/relay.gomiddleware/distributor.gomodel/ability.gomodel/channel.gomodel/pricing.gomodel/task.gopkg/billingexpr/settle.gopkg/billingexpr/types.gorelay/common/relay_info.gorelay/helper/price.goservice/log_info_generate.goservice/quota.goservice/task_billing.gotypes/price_data.goweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/lib/channel-form.tsweb/default/src/features/pricing/lib/price.tsweb/default/src/features/pricing/types.tsweb/default/src/features/usage-logs/components/dialogs/details-dialog.tsxweb/default/src/features/usage-logs/types.tsweb/default/src/i18n/locales/zh.json
| if ( | ||
| other.channel_ratio != null && | ||
| Number.isFinite(other.channel_ratio) && | ||
| other.channel_ratio !== 1 | ||
| ) { | ||
| rows.push({ | ||
| label: t('Channel Ratio'), | ||
| value: `${formatRatio(other.channel_ratio)}x`, | ||
| }) | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify 'Channel Ratio' translation exists in all language files
echo "Checking translation for 'Channel Ratio' in all locale files..."
for lang in en zh fr ru ja vi; do
locale_file="web/default/src/i18n/locales/${lang}.json"
if [ -f "$locale_file" ]; then
if grep -q '"Channel Ratio"' "$locale_file"; then
echo "✓ $lang: translation found"
else
echo "✗ $lang: translation MISSING"
fi
else
echo "✗ $lang: locale file not found"
fi
doneRepository: QuantumNous/new-api
Length of output: 281
Add missing i18n entries for “Channel Ratio”
web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx (lines 220-229) uses t('Channel Ratio'), but the key is missing in locale files for en, fr, ru, ja, and vi (present only in zh). Run bun run i18n:sync from web/default/ (or add the missing keys) to ensure all supported languages render the label.
🤖 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/usage-logs/components/dialogs/details-dialog.tsx`
around lines 220 - 229, The UI uses t('Channel Ratio') in the DetailsDialog rows
push (the block that sets label: t('Channel Ratio')), but that i18n key is
missing in most locale files; add the "Channel Ratio" key with appropriate
translations to all supported locale JSONs (en, fr, ru, ja, vi — matching the
existing keys structure) or run the project's i18n sync command to propagate the
key (e.g., run the i18n:sync script so all locales include the new key), then
verify the DetailsDialog renders the localized label.
411ee11 to
a189cde
Compare
There was a problem hiding this comment.
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 `@service/text_quota.go`:
- Around line 311-323: The code currently applies summary.ChannelRatio early
(setting summary.UpstreamQuota and mutating summary.Quota) which gets
overwritten later by composeTieredTextQuota; instead, stop applying the channel
ratio until after all tiered/surcharge reconciliation is done: remove or revert
the early mutation of summary.Quota in the ChannelRatio block (leave only
summary.ChannelRatio defaulting logic), ensure composeTieredTextQuota runs and
produces the final pre-channel quota (use or introduce a local variable like
finalQuotaBeforeChannelRatio to hold summary.Quota immediately after tiered
reconciliation/composeTieredTextQuota), then set summary.UpstreamQuota =
finalQuotaBeforeChannelRatio and finally apply ChannelRatio once to compute
summary.Quota; also update other["upstream_quota"] to use summary.UpstreamQuota
so upstream logging reflects the true pre-channel amount.
In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 2563-2590: The ratio input currently converts an empty string to 0
via onChange={(e) => field.onChange(Number(e.target.value))}, which causes an
explicit 0 to be stored instead of falling back to the default in
channel-form.ts (ratio: formData.ratio ?? 1); update the Input's onChange in the
FormField for name='ratio' to normalize empty input to undefined (e.g. if
e.target.value === '' then field.onChange(undefined) else
field.onChange(Number(e.target.value))) so clearing the field does not persist a
0 and allows the existing default logic to apply.
🪄 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: 27af2647-dd75-4429-a826-4bb96fe47025
📒 Files selected for processing (23)
constant/context_key.gocontroller/relay.gomiddleware/distributor.gomodel/ability.gomodel/channel.gomodel/pricing.gomodel/task.gopkg/billingexpr/settle.gopkg/billingexpr/types.gorelay/common/relay_info.gorelay/helper/price.goservice/log_info_generate.goservice/quota.goservice/task_billing.goservice/text_quota.gotypes/price_data.goweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/lib/channel-form.tsweb/default/src/features/pricing/lib/price.tsweb/default/src/features/pricing/types.tsweb/default/src/features/usage-logs/components/dialogs/details-dialog.tsxweb/default/src/features/usage-logs/types.tsweb/default/src/i18n/locales/zh.json
✅ Files skipped from review due to trivial changes (2)
- web/default/src/i18n/locales/zh.json
- web/default/src/features/usage-logs/types.ts
🚧 Files skipped from review as they are similar to previous changes (13)
- middleware/distributor.go
- model/task.go
- types/price_data.go
- service/log_info_generate.go
- web/default/src/features/usage-logs/components/dialogs/details-dialog.tsx
- controller/relay.go
- constant/context_key.go
- pkg/billingexpr/types.go
- web/default/src/features/channels/lib/channel-form.ts
- model/ability.go
- service/task_billing.go
- service/quota.go
- model/channel.go
Adds a configurable `ratio` field to channels so that calls routed through different channels for the same model can carry different billing multipliers. Billing formula: quota = tokens × modelRatio × groupRatio × channelRatio - Backend: Channel.Ratio field (GORM auto-migrate, default 1.0); ratio propagated via gin context key set by distributor middleware, read directly in ModelPriceHelper/ModelPriceHelperPerCall before InitChannelMeta is called (fixes timing bug where ChannelMeta was nil); channelRatio applied in PostTextConsumeQuota after all quota sources (token-based and tiered) are resolved to avoid tiered-overwrite bug; upstream_quota (pre-channelRatio) stored in log other for display; audio/WSS/task billing paths updated; log_info_generate emits channel_ratio when != 1.0 - Pricing API: AbilityWithChannel now carries channel_ratio/weight; updatePricing tracks min/max channel ratio per (model,group) and exposes group_channel_ratio_min/max in /api/pricing response - Frontend: channel form gains a ratio input (min 0.01, clears to 1); model marketplace shows price range "$X ~ $Y /M" when channels in the same group carry different ratios, single price otherwise; usage-log detail dialog shows upstream_cost (before channelRatio) and total cost separately when channelRatio != 1; channel ratio 0 treated as 1.0 consistently (|| 1 instead of ?? 1); zh.json translations added Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
a189cde to
58dc2e4
Compare
我自己实际测试的时候 没有被双乘呢。让AI审核了下 你说的问题,好像是不存在的 |

Note
This code was generated and implemented with AI assistance (Claude Sonnet 4.6). The description below has been reviewed and written by the submitter.
📝 变更描述 / Description
新增渠道倍率(Channel Ratio)功能,允许在渠道级别配置独立的计费乘数。
当多个渠道服务同一模型时(如 OpenAI 直连 vs Azure),可以为不同渠道设置不同倍率,以反映实际成本差异,而不必为每个渠道单独分组。
新计费公式:
后端改动:
model/channel.go: 添加Ratio *float64字段(GORM 自动迁移,默认 1.0,nil/0 视为 1.0)BillingSnapshot增加ChannelRatio字段,保证分层表达式计费在结算时使用正确倍率log_info_generate.go:当 channelRatio != 1.0 时写入日志 other 字段Pricing API 改动:
model/ability.go:AbilityWithChannel扩展ChannelRatio和ChannelWeightmodel/pricing.go:updatePricing按权重(channel.weight)加权平均计算每个 (model, group) 的有效渠道倍率,通过group_channel_ratios字段暴露于/api/pricing响应前端改动:
group_channel_ratios,正确反映渠道倍率channel_ratio != 1时展示渠道倍率zh.json添加"渠道倍率"中文翻译向后兼容: 未设置 ratio 的渠道等同于 ratio=1.0,行为与旧版本完全一致。
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
go build及bun run build验证编译通过。📸 运行证明 / Proof of Work
后端编译:
前端编译:
Summary by CodeRabbit