Skip to content

feat: add UTF-8 byte billing mode - #6598

Open
twodogegg wants to merge 1 commit into
QuantumNous:mainfrom
twodogegg:codex/utf8-bytes-billing
Open

feat: add UTF-8 byte billing mode#6598
twodogegg wants to merge 1 commit into
QuantumNous:mainfrom
twodogegg:codex/utf8-bytes-billing

Conversation

@twodogegg

@twodogegg twodogegg commented Aug 2, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

新增通用的 utf8_bytes 计费模式:请求用量按输入文本的 UTF-8 字节数统计,价格仍按每 100 万单位配置。预扣费与结算使用请求级计费模式快照,不受请求期间配置热更新影响,也不会混入输出、缓存、图片或音频 token 用量。

管理后台新增 UTF-8 bytes 定价选项;模型广场和使用日志会显示对应计费单位,并继续使用现有的统一货币换算。$15 / 1M UTF-8 bytes 对应现有 ModelRatio 配置值 7.5

本 PR 由 AI 辅助实现,提交者已检查变更范围和测试结果。

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix) - 请关联对应 Issue,避免将设计取舍、理解偏差或预期不一致直接归类为 bug
  • ✨ 新功能 (New feature) - 重大特性建议先通过 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自整理并撰写此描述,没有直接粘贴未经处理的 AI 输出。
  • 非重复提交: 我已搜索现有的 IssuesPRs,确认不是重复提交。
  • Bug fix 说明: 若此 PR 标记为 Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。
  • 变更理解: 我已理解这些更改的工作原理及可能影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过测试或手动验证,维护者可以据此复核结果。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

  • make test
  • GOWORK=off go vet ./...
  • GOWORK=off go build -buildvcs=false ./...
  • cd relaykit && GOWORK=off go vet ./... && GOWORK=off go build ./...
  • cd web && bun test src/features/system-settings/models/__tests__/utf8-bytes-pricing.test.ts
  • cd web && bun run typecheck
  • 涉及文件通过 oxlint
  • cd web && bun run build && bun run i18n:sync
  • Playwright 验证 s2.1-pro 在模型广场显示 $15 / 1M UTF-8 字节,详情中不显示输出 token 价格

Summary by CodeRabbit

  • New Features

    • Added UTF-8 byte-based billing for model pricing and usage calculation.
    • Added configuration options and pricing previews for input costs per 1M UTF-8 bytes.
    • Updated pricing pages, model details, usage logs, and billing dialogs to display UTF-8 byte units and input-only pricing.
    • Added localized labels across supported languages.
  • Bug Fixes

    • Improved quota calculation and billing behavior for UTF-8 byte-priced models.
    • Ensured UTF-8 billing remains accurate when provider token counts or global token counting settings differ.

@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR adds utf8_bytes billing support across backend pricing and quota processing, administrative pricing configuration, usage logs, pricing displays, translations, and regression tests.

Changes

UTF-8 byte billing

Layer / File(s) Summary
Billing mode contracts and relay propagation
setting/billing_setting/tiered_billing.go, model/pricing.go, controller/ratio_sync.go, relay/common/relay_info.go, controller/relay.go, controller/model_list_test.go
Adds the BillingModeUTF8Bytes constant. Pricing conversion and relay metadata preserve the mode. Model-list tests verify visibility and pricing metadata.
UTF-8 quota processing
relay/helper/price.go, service/token_counter.go, service/text_quota.go
Counts UTF-8 bytes for request estimation. Uses local byte counts for quota settlement and billing-path logging.
Backend billing regression coverage
relay/helper/price_test.go, service/utf8_bytes_billing_test.go
Tests pre-consumption, request estimation, quota settlement, provider adjustment handling, and billing-path logging.
Pricing configuration and snapshots
web/src/features/system-settings/models/*
Adds UTF-8 byte pricing mode selection, input-price editing, persistence, previews, snapshots, labels, and summaries.
Pricing and usage display
web/src/features/pricing/*, web/src/features/usage-logs/*, web/src/i18n/locales/*
Displays UTF-8 byte units, input-only pricing, byte-based usage, billing details, and localized labels.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Relay
  participant EstimateRequestToken
  participant calculateTextQuotaSummary
  participant UsageLog
  Relay->>EstimateRequestToken: Estimate UTF-8 input bytes
  EstimateRequestToken-->>Relay: Return prompt byte count
  Relay->>calculateTextQuotaSummary: Settle local byte usage
  calculateTextQuotaSummary->>UsageLog: Record UTF-8 billing path
Loading

Possibly related PRs

Poem

I’m a rabbit counting bytes in a row,
UTF-8 pricing now has room to grow.
Input is measured, output steps aside,
Clear badges and logs show the ride.
Tiny bytes hop through every flow—
A tidy billing burrow below.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding UTF-8 byte billing support.
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.
✨ 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.

Warning

⚠️ This pull request shows signs of AI-generated slop (redundant_comments, ai_padded_prose). It has been flagged by CodeRabbit slop detection and should be reviewed carefully.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web/src/features/usage-logs/components/dialogs/details-dialog.tsx (1)

228-267: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Exclude non-input billing rows for UTF-8 byte logs.

isUTF8Bytes does not guard the cache, audio, and image row conditions at Lines 302-362. If generic ratio fields exist in a UTF-8 log, the dialog shows prices that this billing mode does not apply. Add !isUTF8Bytes to both enclosing conditions.

Proposed fix
-  if (!isTieredExpr && isClaude && hasAnyCacheTokens(other)) {
+  if (!isTieredExpr && !isUTF8Bytes && isClaude && hasAnyCacheTokens(other)) {
...
-  if (!isTieredExpr) {
+  if (!isTieredExpr && !isUTF8Bytes) {
🤖 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/src/features/usage-logs/components/dialogs/details-dialog.tsx` around
lines 228 - 267, Update the cache, audio, and image row conditions in the
details dialog to also require !isUTF8Bytes, preventing generic ratio-based
billing rows from appearing for UTF-8 byte logs while preserving the existing
input row.
web/src/features/pricing/components/pricing-columns.tsx (1)

178-219: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Hide cached pricing for UTF-8 byte models.

The new branch hides output pricing, but the cached_price cell still formats cache_ratio for UTF-8 models. This conflicts with model-card.tsx and model-details.tsx, which suppress unsupported cache pricing. Add an isUTF8BytesModel(model) check to the cached-price empty condition.

🤖 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/src/features/pricing/components/pricing-columns.tsx` around lines 178 -
219, Update the cached-price cell’s empty-condition in the pricing column
renderer to also treat isUTF8BytesModel(model) as unsupported. Preserve the
existing cache_ratio formatting for models that support cached pricing, while
returning the same empty display used by the other unsupported cases.
🤖 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/token_counter.go`:
- Around line 185-201: Move the RelayFormatOpenAIRealtime zero-return check
before the BillingModeUTF8Bytes branch in the token-counting flow. Update the
surrounding logic in the relevant token-counting function so realtime requests
return zero without billing based on the empty or non-representative
meta.CombineText, while preserving existing behavior for other relay formats.

In `@web/src/features/system-settings/models/model-pricing-snapshots.ts`:
- Around line 83-85: Update getModeLabel so the “UTF-8 bytes” label is produced
through the existing i18next translation mechanism by accepting and using a t
function, or translate it at every render call site. Keep the existing
mode-label behavior unchanged for other values and ensure no user-facing English
string bypasses localization.

---

Outside diff comments:
In `@web/src/features/pricing/components/pricing-columns.tsx`:
- Around line 178-219: Update the cached-price cell’s empty-condition in the
pricing column renderer to also treat isUTF8BytesModel(model) as unsupported.
Preserve the existing cache_ratio formatting for models that support cached
pricing, while returning the same empty display used by the other unsupported
cases.

In `@web/src/features/usage-logs/components/dialogs/details-dialog.tsx`:
- Around line 228-267: Update the cache, audio, and image row conditions in the
details dialog to also require !isUTF8Bytes, preventing generic ratio-based
billing rows from appearing for UTF-8 byte logs while preserving the existing
input row.
🪄 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 Plus

Run ID: 4f32289a-0aa4-4689-b1bf-94891ed3b48c

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab0202 and 8ffc721.

📒 Files selected for processing (31)
  • controller/model_list_test.go
  • controller/ratio_sync.go
  • controller/relay.go
  • model/pricing.go
  • relay/common/relay_info.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • service/text_quota.go
  • service/token_counter.go
  • service/utf8_bytes_billing_test.go
  • setting/billing_setting/tiered_billing.go
  • web/src/features/pricing/components/model-billing-mode-badge.tsx
  • web/src/features/pricing/components/model-card.tsx
  • web/src/features/pricing/components/model-details.tsx
  • web/src/features/pricing/components/pricing-columns.tsx
  • web/src/features/pricing/lib/model-helpers.ts
  • web/src/features/pricing/types.ts
  • web/src/features/system-settings/models/__tests__/utf8-bytes-pricing.test.ts
  • web/src/features/system-settings/models/model-pricing-core.ts
  • web/src/features/system-settings/models/model-pricing-sheet.tsx
  • web/src/features/system-settings/models/model-pricing-snapshots.ts
  • web/src/features/system-settings/models/model-ratio-visual-editor.tsx
  • web/src/features/usage-logs/components/columns/common-logs-columns.tsx
  • web/src/features/usage-logs/components/dialogs/details-dialog.tsx
  • web/src/i18n/locales/en.json
  • web/src/i18n/locales/fr.json
  • web/src/i18n/locales/ja.json
  • web/src/i18n/locales/ru.json
  • web/src/i18n/locales/vi.json
  • web/src/i18n/locales/zh-TW.json
  • web/src/i18n/locales/zh.json

Comment thread service/token_counter.go
Comment on lines +185 to 201
if info.BillingMode == "" {
info.BillingMode = billing_setting.GetBillingMode(info.OriginModelName)
}
if info.BillingMode == billing_setting.BillingModeUTF8Bytes {
count := len(meta.CombineText)
common.SetContextKey(c, constant.ContextKeyPromptTokens, count)
return count, nil
}

// 是否统计token
if !constant.CountToken {
return 0, nil
}

if meta == nil {
return 0, errors.New("token count meta is nil")
}

if info.RelayFormat == types.RelayFormatOpenAIRealtime {
return 0, 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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check how GetTokenCountMeta is implemented for realtime/openai-realtime request types.
rg -n -C5 'func.*GetTokenCountMeta' --type=go
rg -n -C5 'RelayFormatOpenAIRealtime' --type=go relay

Repository: QuantumNous/new-api

Length of output: 157


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== git status/stat =="
git status --short
git diff --stat || true

echo "== locate relevant files =="
fd -a 'token_counter.go|relay.go|types|openai.*realtime|billing_setting' . | sed 's#^\./##' | head -200

echo "== search key identifiers =="
rg -n -C4 'GetTokenCountMeta|RelayFormatOpenAIRealtime|BillingModeUTF8Bytes|CombineText|CountToken' . --glob '*.go' --glob '*.ts' --glob '*.tsx' | head -300

Repository: QuantumNous/new-api

Length of output: 20999


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== service/token_counter.go outline =="
ast-grep outline service/token_counter.go --view compact || true

echo "== service/token_counter.go relevant section =="
sed -n '160,230p' service/token_counter.go

echo "== billing_setting UTF8 bytes definitions/usages =="
rg -n -C4 'BillingModeUTF8Bytes|BilingMode|BillingMode' setting --glob '*.go' | head -200
fd -a '*.go' setting | xargs -r rg -n 'BillingModeUTF8Bytes|AddBillingMode|GetBillingMode|BillingMode' | head -200

echo "== relay valid request relevant section =="
sed -n '35,60p' relay/helper/valid_request.go

echo "== relay pricing relevant section =="
sed -n '80,115p' relay/helper/price.go

Repository: QuantumNous/new-api

Length of output: 7023


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== GetAndValidAudioRequest implementation =="
rg -n -C8 'GetAndValidAudioRequest|Build.*Audio' --glob '*.go' | head -220

echo "== realtime/audio token count metadata in OpenAI relay files =="
fd -a 'openai.*realtime|openai.*audio|audio' relaykit relay controller | sed 's#^\./##'
for f in $(fd -a 'openai.*realtime|openai.*audio|audio' relaykit relay controller); do
  echo "--- $f"
  rg -n -C6 'GetTokenCountMeta|Input|CombineText|openai_realtime|OpenAIRealtime|Audio' "$f" || true
done

echo "== pricing helper pre-consumption context =="
rg -n -C8 'SetEstimatePromptTokens|GetEstimatePromptTokens|PreConsumedTokens|promptTokens|BillingModeUTF8Bytes|RelayFormatOpenAIRealtime' relay --glob '*.go' | head -260

echo "== token_counter EstimateRequestToken continuation =="
sed -n '185,270p' service/token_counter.go

Repository: QuantumNous/new-api

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files mentioning token count meta for realtime/audio =="
rg -n -C6 'RelayFormatOpenAIRealtime|RelayFormatOpenAIAudio|GetTokenCountMeta|CombineText|SetEstimatePromptTokens|GetEstimatePromptTokens' relay --glob '*.go' | head -400

echo "== list relevant relay dto types =="
fd -a 'openai.*realtime|realtime|openai.*audio|audio' relaykit relaykit/dto relay --print0 | xargs -0 -r -I{} sh -c 'echo "--- {}"; wc -l "{}" | sed "s#$# file:"; sed -n "1,220p" "{}"'

echo "== price go relevant =="
sed -n '85,120p' relay/helper/price.go

Repository: QuantumNous/new-api

Length of output: 50375


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== realtime DTO real file =="
sed -n '1,220p' relaykit/dto/realtime.go

echo "== realtime implementation relevant =="
sed -n '1,250p' relay/channel/openai/relay_realtime.go

echo "== pricing preConsume context =="
rg -n 'func preConsumeUsage|preConsumeUsage\(' relay --glob '*.go' -C8

echo "== realtime price/consume helpers =="
rg -n 'RealtimeUsage|audioToken|textToken|input_tokens|output_tokens|InputTokenDetails|Post.*Consume|quota' relay/helper relay channel/relay --glob '*.go' -C5 | head -400

echo "== relevant token counter section exact =="
sed -n '175,205p' service/token_counter.go

Repository: QuantumNous/new-api

Length of output: 41529


Preserve the realtime zero pre-consumer before UTF-8 byte billing.

RelayFormatOpenAIRealtime uses websocket usage accounting through preConsumeUsage; realtime request validation also creates a BaseRequest, so meta.CombineText is empty or not representative. Keep the info.RelayFormat == types.RelayFormatOpenAIRealtime zero-return before the BillingModeUTF8Bytes path, or guard it as a non-billable realtime case.

🤖 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 `@service/token_counter.go` around lines 185 - 201, Move the
RelayFormatOpenAIRealtime zero-return check before the BillingModeUTF8Bytes
branch in the token-counting flow. Update the surrounding logic in the relevant
token-counting function so realtime requests return zero without billing based
on the empty or non-representative meta.CombineText, while preserving existing
behavior for other relay formats.

Comment on lines 83 to +85
export const getModeLabel = (mode?: string) => {
if (mode === 'per-request') return 'Per-request'
if (mode === 'utf8_bytes') return 'UTF-8 bytes'

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

Localize the UTF-8 billing-mode label.

Line 85 adds a user-facing English string without translation. Pass t into getModeLabel, or translate this value at its render call site.

As per coding guidelines, “Frontend user-facing text must use i18next/react-i18next.”

🤖 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/src/features/system-settings/models/model-pricing-snapshots.ts` around
lines 83 - 85, Update getModeLabel so the “UTF-8 bytes” label is produced
through the existing i18next translation mechanism by accepting and using a t
function, or translate it at every render call site. Keep the existing
mode-label behavior unchanged for other values and ensure no user-facing English
string bypasses localization.

Source: Coding guidelines

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