Skip to content

feat: add tiered token pricing support - #4116

Closed
yyhhyyyyyy wants to merge 2 commits into
QuantumNous:mainfrom
yyhhyyyyyy:feat/tiered-token-pricing
Closed

feat: add tiered token pricing support#4116
yyhhyyyyyy wants to merge 2 commits into
QuantumNous:mainfrom
yyhhyyyyyy:feat/tiered-token-pricing

Conversation

@yyhhyyyyyy

@yyhhyyyyyy yyhhyyyyyy commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ 提交警告 / PR Warning

请注意: 请提供人工撰写的简洁摘要。包含大量 AI 灌水内容、逻辑混乱或无视模版的 PR 可能会被无视或直接关闭


💡 沟通提示 / Pre-submission

重大功能变更? 请先提交 Issue 交流,避免无效劳动。

📝 变更描述 / Description

为模型价格配置新增了“阶梯定价”能力,把原来按量计费只能配置一组固定价格/倍率,扩展成可以按 prompt tokens 区间配置多档价格。如 vertex ai 的定价方式。具体实现如下:

  1. 在设置页支持编辑和保存 ModelTierPricing,可以为模型配置多档输入价、输出价和缓存读取价
  2. 后端在实际请求计费时,会根据本次请求的 prompt tokens 命中对应档位,并把该档位换算成当前请求真正使用的价格/倍率和预扣额度
  3. 使用日志和相关测试也一起补齐,方便后续排查实际命中的价格档位和计费结果

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature) - 重大特性建议先 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自撰写此描述,去除了 AI 原始输出的冗余。
  • 深度理解: 我已完全理解这些更改的工作原理及潜在影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过了测试或手动验证。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

image image

Summary by CodeRabbit

Release Notes

  • New Features

    • Tiered pricing support: configure models with token-based pricing tiers to apply different costs based on token thresholds
    • Usage logs now display tier pricing details, including applicable tier information and billing mode indicators
  • Tests

    • Comprehensive test coverage added for tiered pricing configuration, validation, and application

@coderabbitai

coderabbitai Bot commented Apr 6, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Implements tiered pricing for models, allowing step-based pricing calculations based on prompt token thresholds. Adds configuration, application logic, and UI controls for tier-based model pricing across backend services, API helpers, logging, and frontend settings.

Changes

Cohort / File(s) Summary
Type Definitions
types/tier_pricing.go, types/price_data.go
Added new tier pricing types (ModelTierPricingTier, ModelTierPricingConfig, TierPricingMeta) and extended PriceData with optional TierPricing field.
Tier Pricing Core Implementation
setting/ratio_setting/tier_pricing.go, setting/ratio_setting/tier_pricing_test.go
New module implementing full tier pricing lifecycle: JSON parsing, normalization, validation, tier selection logic, and ApplyModelTierPricing function with comprehensive test coverage.
Ratio Setting Integration
setting/ratio_setting/model_ratio.go, setting/ratio_setting/exposed_cache.go
Updated ratio initialization and fallback resolution to support tier pricing; exposed tier pricing data in cache.
Price Calculation Pipeline
relay/helper/price.go, relay/helper/price_test.go
Refactored ModelPriceHelper to apply tier pricing during price computation and added test validating tier-only pricing scenarios.
Quota Calculation Updates
service/quota.go, service/text_quota.go, service/tier_pricing.go
Applied tier pricing before quota calculations in WSS and audio paths; introduced applyTierPricingToRelayInfo helper; added per-tier ratio fields to QuotaInfo.
Test Coverage for Quota
service/text_quota_test.go
Added test for tier pricing re-application during final usage calculations.
Logging & Admin Info
service/log_info_generate.go, service/log_info_generate_test.go, controller/channel-test.go
Added tier pricing details to logging output; extended billing quota type field; added tests for tier pricing log fields.
Configuration Management
controller/option.go, controller/option_test.go, model/option.go, web/src/components/settings/RatioSetting.jsx
Added ModelTierPricing to option keys and update handlers; extended completion ratio metadata to include tier info; added frontend state field.
Frontend Usage Log Display
web/src/helpers/log.js, web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx, web/src/hooks/usage-logs/useUsageLogsData.jsx
Added helpers for formatting tier pricing info, billing quota type detection, and model price derivation; integrated tier pricing display in usage logs table.
Frontend Tier Pricing Editor
web/src/pages/Setting/Ratio/components/ModelPricingEditor.jsx, web/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js
Major UI overhaul: added tier pricing editor modal, tier listing, breakpoint management, validation logic; refactored preview rendering to handle tier-based and flat pricing modes; implemented tier state management and serialization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~28 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion
  • creamlike1024

Poem

🐰 Hop-hop through the token tiers we go,
From shallow streams to pricing flows,
Step by step the costs align,
Claude and Qwen now price so fine!
Tiers stacked up, from low to high,
Hop-hop-hop! ✨ Let's multiply!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 6.45% 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 'feat: add tiered token pricing support' accurately describes the main feature being implemented across the changeset.
Linked Issues check ✅ Passed The PR implements token-based tiered pricing as requested in issue #1664, enabling configurable price brackets by token ranges for models like Claude and Gemini.
Out of Scope Changes check ✅ Passed All changes directly support tiered pricing: backend configuration/application, frontend UI for editing tiers, logging tier details, and comprehensive test coverage.

✏️ 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: 4

🧹 Nitpick comments (1)
service/text_quota_test.go (1)

287-310: Consider test isolation for global tier pricing state.

Both this test and TestModelPriceHelperAllowsTierOnlyPricing modify the global modelTierPricingMap via UpdateModelTierPricingByJSONString. While t.Cleanup resets the state, if tests run in parallel (e.g., via t.Parallel()), they could interfere with each other.

Currently these tests don't call t.Parallel(), so they're safe. If parallelization is added later, consider using unique model names per test to avoid conflicts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/text_quota_test.go` around lines 287 - 310, The test mutates global
modelTierPricingMap via UpdateModelTierPricingByJSONString which can clash with
TestModelPriceHelperAllowsTierOnlyPricing when tests run in parallel; to fix,
make the JSON use a unique model key per test (e.g., include t.Name() or a
random suffix) instead of the shared "google/gemini-3.1-pro-preview", and update
assertions to reference that unique model name so each test modifies isolated
tier pricing while still calling UpdateModelTierPricingByJSONString and
t.Cleanup as before.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@service/quota.go`:
- Around line 140-146: The AudioRatio and AudioCompletionRatio reads currently
trust relayInfo.PriceData unconditionally; update them to use the same fallback
logic used for CompletionRatio (i.e., if
relayInfo.PriceData.AudioRatio/AudioCompletionRatio == 0 &&
relayInfo.PriceData.TierPricing == nil, use the existing fallback value used for
completion/model ratios) so calculateAudioQuota doesn't underbill when
ModelPriceHelper hasn't populated PriceData; apply the same change to the other
occurrences around the blocks that set AudioRatio/AudioCompletionRatio (the
similar sets at the later diff ranges).

In `@setting/ratio_setting/tier_pricing.go`:
- Around line 184-198: The ApplyModelTierPricing function should clamp negative
prompt token counts to zero so callers passing negative values still match the
first tier; at the start of ApplyModelTierPricing (before calling
GetModelTierPricing or iterating config.Tiers) normalize promptTokens = max(0,
promptTokens) (or otherwise set promptTokens to 0 when less than 0) to ensure
tier matching logic that checks tier.MinTokens and tier.MaxTokens behaves
correctly across callers like relay/helper/price.go and service/tier_pricing.go.

In `@web/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js`:
- Around line 764-766: The i18n call t('Legacy Flat Fields') in
useModelPricingEditorState (key: 'legacy-flat-fields') violates the repo
convention that translation keys must be Chinese source strings; replace the
English key with the corresponding Chinese source string (e.g. t('旧版平铺字段') or
the exact Chinese key from web/src/i18n/locales/{lang}.json) and do the same for
the other occurrences referenced around the same area (the other t(...) calls at
the noted nearby spots). Ensure useTranslation() is used in the component and
update the JSON locale files only if the Chinese source key is missing so
t('中文key') resolves correctly.
- Around line 716-741: The current logic divides dependent prices by inputPrice
and skips setting ratios when inputPrice === 0, which causes non-zero dependent
prices to be erased; update the block that computes CompletionRatio, CacheRatio,
CreateCacheRatio, ImageRatio, and AudioRatio so that if inputPrice === 0 you do
NOT overwrite existing model.rawRatios values and you avoid any division-by-zero
(i.e., when inputPrice === 0 and
completionPrice/cachePrice/createCachePrice/imagePrice/audioInputPrice are
non-null, preserve
model.rawRatios.CompletionRatio/CacheRatio/CreateCacheRatio/ImageRatio/AudioRatio
in result instead of setting them to null or attempting division); refer to
symbols result.CompletionRatio, result.CacheRatio, result.CreateCacheRatio,
result.ImageRatio, result.AudioRatio and model.rawRatios to implement the guard
and copy behavior.

---

Nitpick comments:
In `@service/text_quota_test.go`:
- Around line 287-310: The test mutates global modelTierPricingMap via
UpdateModelTierPricingByJSONString which can clash with
TestModelPriceHelperAllowsTierOnlyPricing when tests run in parallel; to fix,
make the JSON use a unique model key per test (e.g., include t.Name() or a
random suffix) instead of the shared "google/gemini-3.1-pro-preview", and update
assertions to reference that unique model name so each test modifies isolated
tier pricing while still calling UpdateModelTierPricingByJSONString and
t.Cleanup as before.
🪄 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: a56c691c-d7fa-4269-9821-359aa39db6dd

📥 Commits

Reviewing files that changed from the base of the PR and between 960bf9c and e19ce2c.

📒 Files selected for processing (24)
  • controller/channel-test.go
  • controller/option.go
  • controller/option_test.go
  • model/option.go
  • relay/helper/price.go
  • relay/helper/price_test.go
  • service/log_info_generate.go
  • service/log_info_generate_test.go
  • service/quota.go
  • service/text_quota.go
  • service/text_quota_test.go
  • service/tier_pricing.go
  • setting/ratio_setting/exposed_cache.go
  • setting/ratio_setting/model_ratio.go
  • setting/ratio_setting/tier_pricing.go
  • setting/ratio_setting/tier_pricing_test.go
  • types/price_data.go
  • types/tier_pricing.go
  • web/src/components/settings/RatioSetting.jsx
  • web/src/components/table/usage-logs/UsageLogsColumnDefs.jsx
  • web/src/helpers/log.js
  • web/src/hooks/usage-logs/useUsageLogsData.jsx
  • web/src/pages/Setting/Ratio/components/ModelPricingEditor.jsx
  • web/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js

Comment thread service/quota.go
Comment on lines +140 to +146
ModelName: modelName,
UsePrice: relayInfo.PriceData.UsePrice,
ModelRatio: modelRatio,
GroupRatio: actualGroupRatio,
CompletionRatio: relayInfo.PriceData.CompletionRatio,
AudioRatio: relayInfo.PriceData.AudioRatio,
AudioCompletionRatio: relayInfo.PriceData.AudioCompletionRatio,

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

Restore the audio-ratio fallback when PriceData is empty.

Unlike modelRatio and completionRatio, these reads now trust relayInfo.PriceData.AudioRatio / AudioCompletionRatio unconditionally. If a realtime/audio path reaches here before ModelPriceHelper has populated them, calculateAudioQuota treats audio tokens as 0x and underbills the request. Apply the same == 0 && TierPricing == nil fallback you already use for CompletionRatio.

💡 Representative fix
audioRatioValue := relayInfo.PriceData.AudioRatio
+if audioRatioValue == 0 && relayInfo.PriceData.TierPricing == nil {
+	audioRatioValue = ratio_setting.GetAudioRatio(modelName)
+}
audioCompletionRatioValue := relayInfo.PriceData.AudioCompletionRatio
+if audioCompletionRatioValue == 0 && relayInfo.PriceData.TierPricing == nil {
+	audioCompletionRatioValue = ratio_setting.GetAudioCompletionRatio(modelName)
+}

Also applies to: 179-187, 290-298

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/quota.go` around lines 140 - 146, The AudioRatio and
AudioCompletionRatio reads currently trust relayInfo.PriceData unconditionally;
update them to use the same fallback logic used for CompletionRatio (i.e., if
relayInfo.PriceData.AudioRatio/AudioCompletionRatio == 0 &&
relayInfo.PriceData.TierPricing == nil, use the existing fallback value used for
completion/model ratios) so calculateAudioQuota doesn't underbill when
ModelPriceHelper hasn't populated PriceData; apply the same change to the other
occurrences around the blocks that set AudioRatio/AudioCompletionRatio (the
similar sets at the later diff ranges).

Comment on lines +184 to +198
func ApplyModelTierPricing(modelName string, priceData types.PriceData, promptTokens int) (types.PriceData, bool) {
config, ok := GetModelTierPricing(modelName)
if !ok || !config.Enabled || config.Basis != TierPricingBasisPromptTokens {
return priceData, false
}

baseCacheRatio := resolveBaseCacheRatio(priceData)

for index, tier := range config.Tiers {
if promptTokens < tier.MinTokens {
continue
}
if tier.MaxTokens != nil && promptTokens >= *tier.MaxTokens {
continue
}

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

Normalize negative prompt counts inside ApplyModelTierPricing.

promptTokens < 0 can never match the first min_tokens == 0 tier, so tier-only callers fall back to the incoming zero-value ratios. service/tier_pricing.go already guards this case before calling here; moving the clamp into the shared helper keeps every caller safe, including relay/helper/price.go.

💡 Proposed fix
func ApplyModelTierPricing(modelName string, priceData types.PriceData, promptTokens int) (types.PriceData, bool) {
+	if promptTokens < 0 {
+		promptTokens = 0
+	}
 	config, ok := GetModelTierPricing(modelName)
 	if !ok || !config.Enabled || config.Basis != TierPricingBasisPromptTokens {
 		return priceData, false
 	}
📝 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
func ApplyModelTierPricing(modelName string, priceData types.PriceData, promptTokens int) (types.PriceData, bool) {
config, ok := GetModelTierPricing(modelName)
if !ok || !config.Enabled || config.Basis != TierPricingBasisPromptTokens {
return priceData, false
}
baseCacheRatio := resolveBaseCacheRatio(priceData)
for index, tier := range config.Tiers {
if promptTokens < tier.MinTokens {
continue
}
if tier.MaxTokens != nil && promptTokens >= *tier.MaxTokens {
continue
}
func ApplyModelTierPricing(modelName string, priceData types.PriceData, promptTokens int) (types.PriceData, bool) {
if promptTokens < 0 {
promptTokens = 0
}
config, ok := GetModelTierPricing(modelName)
if !ok || !config.Enabled || config.Basis != TierPricingBasisPromptTokens {
return priceData, false
}
baseCacheRatio := resolveBaseCacheRatio(priceData)
for index, tier := range config.Tiers {
if promptTokens < tier.MinTokens {
continue
}
if tier.MaxTokens != nil && promptTokens >= *tier.MaxTokens {
continue
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setting/ratio_setting/tier_pricing.go` around lines 184 - 198, The
ApplyModelTierPricing function should clamp negative prompt token counts to zero
so callers passing negative values still match the first tier; at the start of
ApplyModelTierPricing (before calling GetModelTierPricing or iterating
config.Tiers) normalize promptTokens = max(0, promptTokens) (or otherwise set
promptTokens to 0 when less than 0) to ensure tier matching logic that checks
tier.MinTokens and tier.MaxTokens behaves correctly across callers like
relay/helper/price.go and service/tier_pricing.go.

Comment on lines +716 to 741
if (!model.tierPricingEnabled) {
result.ModelRatio = toNormalizedNumber(inputPrice / 2);

if (!model.completionRatioLocked && completionPrice !== null) {
result.CompletionRatio = toNormalizedNumber(completionPrice / inputPrice);
} else if (
model.completionRatioLocked &&
hasValue(model.rawRatios.completionRatio)
) {
result.CompletionRatio = toNormalizedNumber(
model.rawRatios.completionRatio,
);
}
if (cachePrice !== null) {
result.CacheRatio = toNormalizedNumber(cachePrice / inputPrice);
if (!model.completionRatioLocked && completionPrice !== null) {
result.CompletionRatio = toNormalizedNumber(completionPrice / inputPrice);
} else if (
model.completionRatioLocked &&
hasValue(model.rawRatios.completionRatio)
) {
result.CompletionRatio = toNormalizedNumber(
model.rawRatios.completionRatio,
);
}
if (cachePrice !== null) {
result.CacheRatio = toNormalizedNumber(cachePrice / inputPrice);
}
}
if (createCachePrice !== null) {
if (createCachePrice !== null && inputPrice !== 0) {
result.CreateCacheRatio = toNormalizedNumber(createCachePrice / inputPrice);
}
if (imagePrice !== null) {
if (imagePrice !== null && inputPrice !== 0) {
result.ImageRatio = toNormalizedNumber(imagePrice / inputPrice);
}
if (audioInputPrice !== null) {
if (audioInputPrice !== null && inputPrice !== 0) {
result.AudioRatio = toNormalizedNumber(audioInputPrice / inputPrice);
}

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

Don't silently drop dependent prices when the reference input price is 0.

Here completionPrice / inputPrice collapses to null, and the inputPrice !== 0 guards skip CreateCacheRatio / ImageRatio / AudioRatio entirely. Saving a free-input model therefore erases non-zero dependent prices instead of rejecting the unsupported combination. That affects both flat pricing and tier pricing, because tier mode also reuses the first tier's input price as the reference denominator.

💡 Proposed fix
  if (!model.tierPricingEnabled) {
+    if (
+      inputPrice === 0 &&
+      ((completionPrice !== null && completionPrice !== 0) ||
+        (cachePrice !== null && cachePrice !== 0))
+    ) {
+      throw new Error(
+        t('模型 {{name}} 的输入价格为 0 时,输出和缓存读取价格也必须为 0', {
+          name: model.name,
+        }),
+      );
+    }
     result.ModelRatio = toNormalizedNumber(inputPrice / 2);
 
     if (!model.completionRatioLocked && completionPrice !== null) {
       result.CompletionRatio = toNormalizedNumber(completionPrice / inputPrice);
     } else if (
@@
     if (cachePrice !== null) {
       result.CacheRatio = toNormalizedNumber(cachePrice / inputPrice);
     }
   }
+  if (
+    inputPrice === 0 &&
+    [createCachePrice, imagePrice, audioInputPrice].some(
+      (value) => value !== null && value !== 0,
+    )
+  ) {
+    throw new Error(
+      t('模型 {{name}} 的参考输入价格为 0 时,扩展价格也必须为 0', {
+        name: model.name,
+      }),
+    );
+  }
   if (createCachePrice !== null && inputPrice !== 0) {
     result.CreateCacheRatio = toNormalizedNumber(createCachePrice / inputPrice);
   }

Based on learnings, "OpenRouter API can return models with free input pricing (prompt="0") but non-zero completion pricing, requiring division-by-zero guards when computing completion_ratio and cache_ratio in controller/ratio_sync.go."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js` around lines
716 - 741, The current logic divides dependent prices by inputPrice and skips
setting ratios when inputPrice === 0, which causes non-zero dependent prices to
be erased; update the block that computes CompletionRatio, CacheRatio,
CreateCacheRatio, ImageRatio, and AudioRatio so that if inputPrice === 0 you do
NOT overwrite existing model.rawRatios values and you avoid any division-by-zero
(i.e., when inputPrice === 0 and
completionPrice/cachePrice/createCachePrice/imagePrice/audioInputPrice are
non-null, preserve
model.rawRatios.CompletionRatio/CacheRatio/CreateCacheRatio/ImageRatio/AudioRatio
in result instead of setting them to null or attempting division); refer to
symbols result.CompletionRatio, result.CacheRatio, result.CreateCacheRatio,
result.ImageRatio, result.AudioRatio and model.rawRatios to implement the guard
and copy behavior.

Comment on lines +764 to +766
key: 'legacy-flat-fields',
title: t('Legacy Flat Fields'),
rows: [

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 | 🟡 Minor

Use a Chinese source key for this label.

t('Legacy Flat Fields') breaks the repo's i18n key convention and is unlikely to resolve from the existing locale JSON files.

As per coding guidelines, "web/src/**/*.{ts,tsx,js,jsx}: Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components."

Also applies to: 808-810, 862-864, 928-930

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Ratio/hooks/useModelPricingEditorState.js` around lines
764 - 766, The i18n call t('Legacy Flat Fields') in useModelPricingEditorState
(key: 'legacy-flat-fields') violates the repo convention that translation keys
must be Chinese source strings; replace the English key with the corresponding
Chinese source string (e.g. t('旧版平铺字段') or the exact Chinese key from
web/src/i18n/locales/{lang}.json) and do the same for the other occurrences
referenced around the same area (the other t(...) calls at the noted nearby
spots). Ensure useTranslation() is used in the component and update the JSON
locale files only if the Chinese source key is missing so t('中文key') resolves
correctly.

@yyhhyyyyyy yyhhyyyyyy closed this Apr 7, 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.

请问能跟进tokens阶梯计费吗?

1 participant