Skip to content

feat: sync upstream pricing from pricing endpoint - #4452

Merged
Calcium-Ion merged 5 commits into
QuantumNous:mainfrom
seefs001:feature/sync-upstream-price
Apr 26, 2026
Merged

feat: sync upstream pricing from pricing endpoint#4452
Calcium-Ion merged 5 commits into
QuantumNous:mainfrom
seefs001:feature/sync-upstream-price

Conversation

@seefs001

@seefs001 seefs001 commented Apr 25, 2026

Copy link
Copy Markdown
Collaborator

⚠️ 提交说明 / PR Notice

Important

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

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

上游价格同步支持表达式计费同步,优化UI/UX

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
image

Summary by CodeRabbit

Release Notes

  • New Features

    • Expanded upstream synchronization to include additional pricing and billing fields: image ratio, audio ratio, audio completion ratio, billing mode, and billing expression.
    • Reorganized sync table to display multiple fields per model.
  • Style

    • Updated sync label from "upstream multiplier sync" to "upstream price sync."
    • Enhanced loading states and control responsiveness during synchronization operations.

@coderabbitai

coderabbitai Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

The upstream synchronization system is expanded from simple ratio syncing to comprehensive pricing and billing sync. The default backend endpoint transitions from /api/ratio_config to /api/pricing, and synced fields now include billing settings (billing_mode, billing_expr) alongside additional ratio types (create_cache, image, audio, audio_completion). Backend sync logic and frontend UI are refactored to handle the broader field set across multiple layers.

Changes

Cohort / File(s) Summary
Backend Sync Engine
controller/ratio_sync.go
Expanded sync target to /api/pricing endpoint with new field set including billing and additional ratio types. Refactored type detection, upstream parsing, and local data feeding to handle varying data types and maps. Replaced exact equality confidence check with numeric tolerance comparison.
Backend Settings & Data Exposure
setting/billing_setting/tiered_billing.go, setting/ratio_setting/model_ratio.go
Added exported constants for billing field names and new getter functions that expose read-only snapshot copies of in-memory billing modes, expressions, and ratio maps for integration with sync mechanism.
Frontend Configuration
web/src/constants/common.constant.js
Changed default endpoint constant from /api/ratio_config to /api/pricing to align with backend sync target.
Frontend UI Components
web/src/components/settings/ChannelSelectorModal.jsx, web/src/components/settings/RatioSetting.jsx
Reordered option entry in endpoint selector and updated upstream sync tab label from "upstream multiplier sync" to "upstream price sync".
Frontend Sync UI
web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx
Refactored table structure from per-ratioType rows to per-model rows with multiple selectable fields. Added UI support for billing fields and new ratio types, enhanced diff display, implemented source-preferred selection logic with paired field auto-population, and added loading states during sync operations.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as UpstreamRatioSync<br/>(React)
    participant API as Backend<br/>/api/pricing
    participant Sync as Sync Logic<br/>(ratio_sync.go)
    participant Settings as Settings<br/>(ratio/billing)

    User->>UI: Trigger upstream sync
    UI->>UI: Show loading spinner
    
    UI->>API: GET /api/pricing
    API->>API: Parse upstream pricing/billing data
    API-->>UI: Return expanded field set<br/>(ratios + billing_mode/expr)
    
    UI->>Settings: Fetch local pricing sync data<br/>(ratios + billing data)
    Settings-->>UI: Return merged local state
    
    UI->>UI: Build differences for each field<br/>(model_ratio, billing_mode, etc.)
    UI->>UI: Display table with current vs upstream
    
    User->>UI: Select upstream values + prefer source
    UI->>Sync: POST with selected fields<br/>+ parsed values + preferences
    
    Sync->>Settings: Update ratios via setting
    Sync->>Settings: Update billing via setting
    Sync-->>UI: Return success flag
    
    UI->>UI: Dismiss loading, show success toast
    User-->>UI: View confirmation modal
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion

Poem

🐰 A rabbit hops through pricing fields so new,
Billing modes and ratios, synced true!
From ratios alone to prices complete,
Upstream data flows—so very neat! 🎯✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% 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
Title check ✅ Passed The title accurately describes the main objective of the pull request: syncing upstream pricing from the pricing endpoint, which is reflected in the code changes across backend sync logic and frontend UI components.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

Caution

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

⚠️ Outside diff range comments (1)
web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx (1)

481-498: ⚠️ Potential issue | 🟡 Minor

Conflict modal description only surfaces model_ratio/completion_ratio, missing the new ratio fields.

The new sync surface includes cache_ratio, create_cache_ratio, image_ratio, audio_ratio, and audio_completion_ratio, but the conflict description for localCat === 'ratio' (Line 484) only formats ModelRatio + CompletionRatio. Likewise, when newCat === 'ratio' only model_ratio / completion_ratio are shown (Lines 490–492). If a model has e.g. only an ImageRatio configured locally and an upstream that switches it to model_price, the user sees 模型倍率: - / 补全倍率: - and can't tell what is actually being replaced.

Consider iterating ratioSyncFields to build the description so it reflects whichever fields actually have local/new values.

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

In `@web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx` around lines 481 - 498,
The conflict description currently hardcodes ModelRatio/CompletionRatio in
currentDesc and newDesc when localCat/newCat === 'ratio'; instead iterate the
shared ratioSyncFields array (or create it if missing) to build descriptions
dynamically: for currentDesc when localCat === 'ratio' map over ratioSyncFields
and for each field read currentRatios[field][model] (fallback to '-') and format
as `${t(fieldLabel)} : ${value}`, join with '\n'; similarly for newDesc when
newCat === 'ratio' map ratioSyncFields and read from ratios[field_key] (fallback
to '-') to produce the new description; keep existing behavior for 'price' and
preserve channels computation using findSourceChannel.
🧹 Nitpick comments (6)
controller/ratio_sync.go (2)

446-489: Inconsistent map-conversion style — prefer valueMap everywhere.

cache_ratio, create_cache_ratio, image_ratio, audio_ratio, audio_completion_ratio, billing_mode, and billing_expr are converted with the new valueMap helper, while model_ratio, completion_ratio, and model_price still hand-roll the map[string]float64 → map[string]any conversion. Unifying both paths makes the block easier to read and keeps future field additions consistent.

♻️ Suggested change
-			if len(modelRatioMap) > 0 {
-				ratioAny := make(map[string]any, len(modelRatioMap))
-				for k, v := range modelRatioMap {
-					ratioAny[k] = v
-				}
-				converted["model_ratio"] = ratioAny
-			}
-
-			if len(completionRatioMap) > 0 {
-				compAny := make(map[string]any, len(completionRatioMap))
-				for k, v := range completionRatioMap {
-					compAny[k] = v
-				}
-				converted["completion_ratio"] = compAny
-			}
+			if len(modelRatioMap) > 0 {
+				converted["model_ratio"] = valueMap(modelRatioMap)
+			}
+			if len(completionRatioMap) > 0 {
+				converted["completion_ratio"] = valueMap(completionRatioMap)
+			}
@@
-			if len(modelPriceMap) > 0 {
-				priceAny := make(map[string]any, len(modelPriceMap))
-				for k, v := range modelPriceMap {
-					priceAny[k] = v
-				}
-				converted["model_price"] = priceAny
-			}
+			if len(modelPriceMap) > 0 {
+				converted["model_price"] = valueMap(modelPriceMap)
+			}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/ratio_sync.go` around lines 446 - 489, Replace the hand-rolled
conversions for model_ratio, completion_ratio, and model_price with the existing
valueMap helper for consistency: instead of creating priceAny/ratioAny maps and
looping over modelPriceMap/modelRatioMap/completionRatioMap, call
valueMap(modelPriceMap), valueMap(modelRatioMap), and
valueMap(completionRatioMap) and assign those to converted["model_price"],
converted["model_ratio"], and converted["completion_ratio"] respectively; keep
the existing usage of billing_setting.BillingModeField and
billing_setting.BillingExprField unchanged.

64-86: LGTM on the field schema — keep numeric/non-numeric lists in sync.

pricingSyncFields and numericPricingSyncFields correctly partition into ratio/price (numeric) and billing_mode/billing_expr (string). Just be aware future fields must be added to both lists if they're numeric, otherwise normalizeSyncValue will pass through the raw json.Number/typed value and valuesEqual's float64 fast-path won't trigger. Worth a one-line comment above the schema to remind contributors.

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

In `@controller/ratio_sync.go` around lines 64 - 86, Add a one-line comment above
the pricingSyncFields/numericPricingSyncFields block reminding maintainers to
keep both lists in sync for numeric fields; also mention that numeric fields
must be included in numericPricingSyncFields so normalizeSyncValue and
valuesEqual (the float64 fast-path) behave correctly. Reference the arrays
pricingSyncFields and numericPricingSyncFields and the functions
normalizeSyncValue and valuesEqual in the comment so future contributors see
where to update when adding new numeric pricing fields.
setting/billing_setting/tiered_billing.go (1)

50-56: Optional: prefer maps.Clone over lo.Assign for a single-source copy.

lo.Assign(billingSetting.BillingMode) works, but maps.Clone (Go 1.21+) is the idiomatic way to clone a single map and avoids pulling in the variadic merge path. Same applies to model_ratio.go's reliance on RWMap.ReadAll.

♻️ Suggested change
-import (
-	"fmt"
-
-	"github.com/QuantumNous/new-api/pkg/billingexpr"
-	"github.com/QuantumNous/new-api/setting/config"
-	"github.com/samber/lo"
-)
+import (
+	"fmt"
+	"maps"
+
+	"github.com/QuantumNous/new-api/pkg/billingexpr"
+	"github.com/QuantumNous/new-api/setting/config"
+	"github.com/samber/lo"
+)
@@
-func GetBillingModeCopy() map[string]string {
-	return lo.Assign(billingSetting.BillingMode)
-}
-
-func GetBillingExprCopy() map[string]string {
-	return lo.Assign(billingSetting.BillingExpr)
-}
+func GetBillingModeCopy() map[string]string {
+	return maps.Clone(billingSetting.BillingMode)
+}
+
+func GetBillingExprCopy() map[string]string {
+	return maps.Clone(billingSetting.BillingExpr)
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@setting/billing_setting/tiered_billing.go` around lines 50 - 56, Replace the
use of lo.Assign in GetBillingModeCopy and GetBillingExprCopy with the Go 1.21
idiomatic maps.Clone to create a shallow copy of the single source map (use
maps.Clone(billingSetting.BillingMode) and
maps.Clone(billingSetting.BillingExpr)); also audit model_ratio.go where
RWMap.ReadAll is used and prefer the simpler maps.Clone or the map's native
cloning approach when copying a single map to avoid the variadic/merge semantics
of lo.Assign.
web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx (3)

743-744: Stale dependency in upstreamNames useMemo.

useMemo's dependency array (Line 744) lists [filteredDataSource, ratioTypeFilter], but filteredDataSource is already derived from ratioTypeFilter, so listing it again is redundant. Either drop ratioTypeFilter or rely on filteredDataSource alone for clarity.

♻️ Suggested change
-    }, [filteredDataSource, ratioTypeFilter]);
+    }, [filteredDataSource]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx` around lines 743 - 744,
The upstreamNames useMemo has a redundant dependency: remove ratioTypeFilter
from the dependency array and depend only on filteredDataSource (i.e., change
the dependency list for the upstreamNames useMemo to [filteredDataSource]) so
the memoization is driven solely by the derived data; update the upstreamNames
useMemo (function named upstreamNames) to rely on filteredDataSource only and
ensure no other direct references to ratioTypeFilter remain inside that memo.

264-341: Hoist field schema and helper functions out of the component body.

ratioSyncFields, numericSyncFields, syncFieldOrder, getSyncFieldLabel, getOrderedRatioTypes, deleteResolutionField, getBillingCategory, optionKeyBySyncField, getUpstreamValue, isSelectableUpstreamValue, getPreferredSyncField, and shouldShowSyncField are recreated on every render. Pure ones (everything except getSyncFieldLabel, which closes over t) can live at module scope. Even the label function can take t as a parameter or be wrapped in a useMemo. This will both reduce GC churn and make the file easier to test in isolation.

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

In `@web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx` around lines 264 - 341,
The listed constants and pure helper functions (ratioSyncFields,
numericSyncFields, syncFieldOrder, getOrderedRatioTypes, deleteResolutionField,
getBillingCategory, optionKeyBySyncField, getUpstreamValue,
isSelectableUpstreamValue, getPreferredSyncField, shouldShowSyncField) should be
hoisted to module scope to avoid re-creating them each render; move their
definitions out of the component body and keep their signatures the same, then
update component references accordingly. For getSyncFieldLabel (which currently
closes over t), either change it to accept t as an explicit parameter
(getSyncFieldLabel(t, ratioType)) and call it from the component, or wrap it in
useMemo inside the component to memoize the function; ensure any calls are
updated to the new signature or the memoized value. Finally, run a quick
search-and-replace to update all in-component references to the hoisted
functions/constants (e.g., getOrderedRatioTypes, deleteResolutionField,
optionKeyBySyncField) so imports/uses still match.

282-296: Run i18n extraction for the newly added translation keys.

This file introduces several new t('...') keys (缓存创建倍率, 图片倍率, 音频倍率, 音频补全倍率, 计费模式, 表达式计费, 按价格字段筛选, 当前价格, 未找到差异化价格,无需同步, 暂无差异化价格显示, 正在同步价格,请稍候, 正在同步上游价格,请稍候, 该模型存在固定价格与倍率计费方式冲突,请确认选择, 该数据可能不可信,请谨慎使用, 未设置, 与本地相同, 当前计费). Make sure they're synced into web/src/i18n/locales/{lang}.json and unused old keys (e.g., 按倍率类型筛选, 当前倍率) are removed.

As per coding guidelines, "Use CLI tools: bun run i18n:extract, bun run i18n:sync, bun run i18n:lint."

#!/bin/bash
# Spot-check whether the new keys exist in locale files and whether the old ones still leak.
old_keys=(按倍率类型筛选 当前倍率 上游倍率同步)
new_keys=(缓存创建倍率 图片倍率 音频倍率 音频补全倍率 计费模式 表达式计费 按价格字段筛选 当前价格 当前计费 与本地相同 未设置 上游价格同步)
for f in $(fd -e json . web/src/i18n/locales); do
  echo "==> $f"
  for k in "${old_keys[@]}"; do
    if jq -e --arg k "$k" 'has($k)' "$f" >/dev/null; then echo "  STILL HAS old key: $k"; fi
  done
  for k in "${new_keys[@]}"; do
    if ! jq -e --arg k "$k" 'has($k)' "$f" >/dev/null; then echo "  MISSING new key: $k"; fi
  done
done
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx` around lines 282 - 296,
The new UI strings introduced in getSyncFieldLabel (calls to
t('缓存创建倍率','图片倍率','音频倍率','音频补全倍率','计费模式','表达式计费', etc.) must be added to all
locale JSONs and obsolete keys removed: run the i18n CLI pipeline (bun run
i18n:extract, bun run i18n:sync, bun run i18n:lint) to extract and sync the new
keys into web/src/i18n/locales/{lang}.json, verify the listed new keys (包括
按价格字段筛选 当前价格 未找到差异化价格,无需同步 暂无差异化价格显示 正在同步价格,请稍候 正在同步上游价格,请稍候
该模型存在固定价格与倍率计费方式冲突,请确认选择 该数据可能不可信,请谨慎使用 未设置 与本地相同 当前计费) are present, and remove
old/unneeded keys (例如 按倍率类型筛选 当前倍率 上游倍率同步) from the locale files; re-run the
lint step to ensure no missing/unused keys and commit the updated locale JSONs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/ratio_sync.go`:
- Around line 412-442: The loop over pricingItems currently only sets
billingModeMap and billingExprMap when item.BillingMode ==
billing_setting.BillingModeTieredExpr and billingExpr is non-empty, which drops
upstream changes that flip a model back to ratio; modify the logic inside the
pricingItems iteration (the block that writes to billingModeMap and
billingExprMap) to always record the upstream billing mode for the model (e.g.,
set billingModeMap[item.ModelName] = item.BillingMode) and only set
billingExprMap when BillingMode == BillingModeTieredExpr and billingExpr is
non-empty, so a switch from tiered_expr back to ratio is propagated and stored
state can be cleared/updated accordingly.
- Around line 134-140: getLocalPricingSyncData currently merges a 30s TTL-cached
payload from ratio_setting.GetExposedData() with live reads from
ratio_setting.GetImageRatioCopy(), GetAudioRatioCopy(), and
GetAudioCompletionRatioCopy(), causing possible staleness asymmetry; fix by
choosing one consistency approach: either replace the cached call and read all
ratio fields live (call the live getters for model_ratio as well) so
getLocalPricingSyncData only uses RWMap live reads, or extend the exposed cache
in ratio_setting to include image/audio/audio_completion fields (update the
exposed_cache logic and its payload) and keep using GetExposedData(); update
callers to use the same unified source and remove mixed-source merging in
getLocalPricingSyncData.

In `@setting/billing_setting/tiered_billing.go`:
- Around line 50-67: The current GetBillingModeCopy and GetBillingExprCopy call
lo.Assign on billingSetting.BillingMode and BillingExpr which can be
concurrently written by config.GlobalConfig.LoadFromDB (via
updateConfigFromMap), causing fatal concurrent map access; fix by protecting
these maps either by converting them to types.RWMap[string,string] (as in
setting/ratio_setting/model_ratio.go) and replacing direct map usage with
RWMap.Load/Clone calls in GetBillingModeCopy/GetBillingExprCopy and callers like
GetPricingSyncData, or add a package-level sync.RWMutex around billingSetting
and update all accessors and writers (including updateConfigFromMap and
LoadFromDB paths) to use RLock/RUnlock for reads and Lock/Unlock for writes so
lo.Assign never iterates an unprotected map.

In `@web/src/components/settings/RatioSetting.jsx`:
- Line 109: The tab label key was changed in RatioSetting.jsx (Tabs.TabPane with
itemKey 'upstream_sync') from '上游倍率同步' to '上游价格同步' but locale JSONs weren’t
updated; add the new key '上游价格同步' with appropriate translations to every locale
file in web/src/i18n/locales/{lang}.json, remove the obsolete '上游倍率同步' entries,
and then run "bun run i18n:sync" to sync translation files.

In `@web/src/constants/common.constant.js`:
- Line 22: Changing DEFAULT_ENDPOINT to '/api/pricing' will silently break
channels that never set an endpoint because UpstreamRatioSync.jsx only preserves
previously-configured endpoints via the channelEndpoints merge; to fix, either
(A) revert DEFAULT_ENDPOINT to '/api/ratio_config' or (B) implement a safer
fallback in the code that resolves endpoints (e.g., in the logic around
channelEndpoints/UpstreamRatioSync.jsx) to try '/api/ratio_config' if a request
to '/api/pricing' returns 404/unsupported, and add a UI hint or release-note
message informing users of the new default; update references to
DEFAULT_ENDPOINT and the endpoint-resolution logic in UpstreamRatioSync.jsx
accordingly.

---

Outside diff comments:
In `@web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx`:
- Around line 481-498: The conflict description currently hardcodes
ModelRatio/CompletionRatio in currentDesc and newDesc when localCat/newCat ===
'ratio'; instead iterate the shared ratioSyncFields array (or create it if
missing) to build descriptions dynamically: for currentDesc when localCat ===
'ratio' map over ratioSyncFields and for each field read
currentRatios[field][model] (fallback to '-') and format as `${t(fieldLabel)} :
${value}`, join with '\n'; similarly for newDesc when newCat === 'ratio' map
ratioSyncFields and read from ratios[field_key] (fallback to '-') to produce the
new description; keep existing behavior for 'price' and preserve channels
computation using findSourceChannel.

---

Nitpick comments:
In `@controller/ratio_sync.go`:
- Around line 446-489: Replace the hand-rolled conversions for model_ratio,
completion_ratio, and model_price with the existing valueMap helper for
consistency: instead of creating priceAny/ratioAny maps and looping over
modelPriceMap/modelRatioMap/completionRatioMap, call valueMap(modelPriceMap),
valueMap(modelRatioMap), and valueMap(completionRatioMap) and assign those to
converted["model_price"], converted["model_ratio"], and
converted["completion_ratio"] respectively; keep the existing usage of
billing_setting.BillingModeField and billing_setting.BillingExprField unchanged.
- Around line 64-86: Add a one-line comment above the
pricingSyncFields/numericPricingSyncFields block reminding maintainers to keep
both lists in sync for numeric fields; also mention that numeric fields must be
included in numericPricingSyncFields so normalizeSyncValue and valuesEqual (the
float64 fast-path) behave correctly. Reference the arrays pricingSyncFields and
numericPricingSyncFields and the functions normalizeSyncValue and valuesEqual in
the comment so future contributors see where to update when adding new numeric
pricing fields.

In `@setting/billing_setting/tiered_billing.go`:
- Around line 50-56: Replace the use of lo.Assign in GetBillingModeCopy and
GetBillingExprCopy with the Go 1.21 idiomatic maps.Clone to create a shallow
copy of the single source map (use maps.Clone(billingSetting.BillingMode) and
maps.Clone(billingSetting.BillingExpr)); also audit model_ratio.go where
RWMap.ReadAll is used and prefer the simpler maps.Clone or the map's native
cloning approach when copying a single map to avoid the variadic/merge semantics
of lo.Assign.

In `@web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx`:
- Around line 743-744: The upstreamNames useMemo has a redundant dependency:
remove ratioTypeFilter from the dependency array and depend only on
filteredDataSource (i.e., change the dependency list for the upstreamNames
useMemo to [filteredDataSource]) so the memoization is driven solely by the
derived data; update the upstreamNames useMemo (function named upstreamNames) to
rely on filteredDataSource only and ensure no other direct references to
ratioTypeFilter remain inside that memo.
- Around line 264-341: The listed constants and pure helper functions
(ratioSyncFields, numericSyncFields, syncFieldOrder, getOrderedRatioTypes,
deleteResolutionField, getBillingCategory, optionKeyBySyncField,
getUpstreamValue, isSelectableUpstreamValue, getPreferredSyncField,
shouldShowSyncField) should be hoisted to module scope to avoid re-creating them
each render; move their definitions out of the component body and keep their
signatures the same, then update component references accordingly. For
getSyncFieldLabel (which currently closes over t), either change it to accept t
as an explicit parameter (getSyncFieldLabel(t, ratioType)) and call it from the
component, or wrap it in useMemo inside the component to memoize the function;
ensure any calls are updated to the new signature or the memoized value.
Finally, run a quick search-and-replace to update all in-component references to
the hoisted functions/constants (e.g., getOrderedRatioTypes,
deleteResolutionField, optionKeyBySyncField) so imports/uses still match.
- Around line 282-296: The new UI strings introduced in getSyncFieldLabel (calls
to t('缓存创建倍率','图片倍率','音频倍率','音频补全倍率','计费模式','表达式计费', etc.) must be added to all
locale JSONs and obsolete keys removed: run the i18n CLI pipeline (bun run
i18n:extract, bun run i18n:sync, bun run i18n:lint) to extract and sync the new
keys into web/src/i18n/locales/{lang}.json, verify the listed new keys (包括
按价格字段筛选 当前价格 未找到差异化价格,无需同步 暂无差异化价格显示 正在同步价格,请稍候 正在同步上游价格,请稍候
该模型存在固定价格与倍率计费方式冲突,请确认选择 该数据可能不可信,请谨慎使用 未设置 与本地相同 当前计费) are present, and remove
old/unneeded keys (例如 按倍率类型筛选 当前倍率 上游倍率同步) from the locale files; re-run the
lint step to ensure no missing/unused keys and commit the updated locale JSONs.
🪄 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: 6ee4d120-40cb-48c2-b619-e3d6f700fe70

📥 Commits

Reviewing files that changed from the base of the PR and between f2f3410 and 06897e4.

📒 Files selected for processing (7)
  • controller/ratio_sync.go
  • setting/billing_setting/tiered_billing.go
  • setting/ratio_setting/model_ratio.go
  • web/src/components/settings/ChannelSelectorModal.jsx
  • web/src/components/settings/RatioSetting.jsx
  • web/src/constants/common.constant.js
  • web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx

Comment thread controller/ratio_sync.go
Comment on lines +134 to +140
func getLocalPricingSyncData() map[string]any {
data := billing_setting.GetPricingSyncData(map[string]any(ratio_setting.GetExposedData()))
data["image_ratio"] = ratio_setting.GetImageRatioCopy()
data["audio_ratio"] = ratio_setting.GetAudioRatioCopy()
data["audio_completion_ratio"] = ratio_setting.GetAudioCompletionRatioCopy()
return data
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm exposedDataTTL and that the new ratio fields are not part of the cached payload.
rg -nP -C5 'exposedDataTTL|exposedCache|GetExposedData' --type=go

Repository: QuantumNous/new-api

Length of output: 4388


🏁 Script executed:

# Find implementations of the new ratio getter functions
rg -nP 'func.*GetImageRatioCopy|func.*GetAudioRatioCopy|func.*GetAudioCompletionRatioCopy' --type=go -A 10

Repository: QuantumNous/new-api

Length of output: 1488


🏁 Script executed:

# Also check the sync mechanism and how frequently getLocalPricingSyncData is called
rg -nP 'getLocalPricingSyncData|syncPricing|SyncPricing' --type=go -C 3

Repository: QuantumNous/new-api

Length of output: 876


🏁 Script executed:

# Find the RWMap definition and ReadAll implementation
rg -nP 'type RWMap|func.*ReadAll\(\)' --type=go -A 5 | head -80

Repository: QuantumNous/new-api

Length of output: 585


🏁 Script executed:

# Find where imageRatioMap is defined and how it's updated
rg -nP 'imageRatioMap\s*=' --type=go -C 3

Repository: QuantumNous/new-api

Length of output: 588


🏁 Script executed:

# Check the GetPricingSyncData function to understand data flow
rg -nP 'func.*GetPricingSyncData' --type=go -A 20

Repository: QuantumNous/new-api

Length of output: 1695


🏁 Script executed:

# Look at how sync data is used in the sync process
sed -n '498,550p' controller/ratio_sync.go

Repository: QuantumNous/new-api

Length of output: 1285


getLocalPricingSyncData mixes cached and live snapshots; verify staleness assumptions.

ratio_setting.GetExposedData() returns a TTL-cached payload (30 seconds per setting/ratio_setting/exposed_cache.go), but GetImageRatioCopy(), GetAudioRatioCopy(), and GetAudioCompletionRatioCopy() always read live from their respective RWMap instances. Within a single sync request, this means model_ratio may be up to 30 seconds stale while the new ratio fields are current, creating a consistency asymmetry.

If consistency is desirable, either fetch all ratios live here (bypassing GetExposedData()'s cache) or extend the exposed cache to include the new ratio fields.

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

In `@controller/ratio_sync.go` around lines 134 - 140, getLocalPricingSyncData
currently merges a 30s TTL-cached payload from ratio_setting.GetExposedData()
with live reads from ratio_setting.GetImageRatioCopy(), GetAudioRatioCopy(), and
GetAudioCompletionRatioCopy(), causing possible staleness asymmetry; fix by
choosing one consistency approach: either replace the cached call and read all
ratio fields live (call the live getters for model_ratio as well) so
getLocalPricingSyncData only uses RWMap live reads, or extend the exposed cache
in ratio_setting to include image/audio/audio_completion fields (update the
exposed_cache logic and its payload) and keep using GetExposedData(); update
callers to use the same unified source and remove mixed-source merging in
getLocalPricingSyncData.

Comment thread controller/ratio_sync.go
Comment on lines 412 to 442
for _, item := range pricingItems {
if item.ModelName == "" {
continue
}
if item.BillingMode == billing_setting.BillingModeTieredExpr && strings.TrimSpace(item.BillingExpr) != "" {
billingModeMap[item.ModelName] = billing_setting.BillingModeTieredExpr
billingExprMap[item.ModelName] = item.BillingExpr
}
if item.QuotaType == 1 {
modelPriceMap[item.ModelName] = item.ModelPrice
} else {
modelRatioMap[item.ModelName] = item.ModelRatio
// completionRatio 可能为 0,此时也直接赋值,保持与上游一致
completionRatioMap[item.ModelName] = item.CompletionRatio
}
if item.CacheRatio != nil {
cacheRatioMap[item.ModelName] = *item.CacheRatio
}
if item.CreateCacheRatio != nil {
createCacheRatioMap[item.ModelName] = *item.CreateCacheRatio
}
if item.ImageRatio != nil {
imageRatioMap[item.ModelName] = *item.ImageRatio
}
if item.AudioRatio != nil {
audioRatioMap[item.ModelName] = *item.AudioRatio
}
if item.AudioCompletionRatio != nil {
audioCompletionRatioMap[item.ModelName] = *item.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 | 🟡 Minor

Upstream billing_mode is only forwarded when it equals tiered_expr with a non-empty expression.

The condition on Line 416-419 silently drops every other billing_mode value from the upstream response. Consequences:

  • If an upstream model has billing_mode = "ratio" (the implicit default), nothing about its billing mode is recorded — fine, since the local default is also ratio.
  • However, if a model previously synced as tiered_expr upstream is now set back to ratio upstream, that "switch back" signal is dropped. The diff will show no change for billing_mode, so the user has no way to revert through the sync flow — they'd have to manually clear the local billing_setting.billing_mode entry.

If this is intentional (i.e. sync only adds expression-based billing, never removes it), please document it; otherwise consider also propagating billing_mode = "ratio" so removals can flow through.

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

In `@controller/ratio_sync.go` around lines 412 - 442, The loop over pricingItems
currently only sets billingModeMap and billingExprMap when item.BillingMode ==
billing_setting.BillingModeTieredExpr and billingExpr is non-empty, which drops
upstream changes that flip a model back to ratio; modify the logic inside the
pricingItems iteration (the block that writes to billingModeMap and
billingExprMap) to always record the upstream billing mode for the model (e.g.,
set billingModeMap[item.ModelName] = item.BillingMode) and only set
billingExprMap when BillingMode == BillingModeTieredExpr and billingExpr is
non-empty, so a switch from tiered_expr back to ratio is propagated and stored
state can be cleared/updated accordingly.

Comment on lines +50 to +67
func GetBillingModeCopy() map[string]string {
return lo.Assign(billingSetting.BillingMode)
}

func GetBillingExprCopy() map[string]string {
return lo.Assign(billingSetting.BillingExpr)
}

func GetPricingSyncData(base map[string]any) map[string]any {
extra := make(map[string]any, 2)
if modes := GetBillingModeCopy(); len(modes) > 0 {
extra[BillingModeField] = modes
}
if exprs := GetBillingExprCopy(); len(exprs) > 0 {
extra[BillingExprField] = exprs
}
return lo.Assign(base, extra)
}

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look for write sites to billingSetting.BillingMode / BillingExpr to assess whether
# concurrent mutation can happen at runtime.
rg -nP -C3 '\bbillingSetting\.(BillingMode|BillingExpr)\b'
rg -nP -C3 'billing_setting\.(BillingMode|BillingExpr)\b'
# Find where billing_setting config is reloaded / unmarshalled
rg -nP -C3 '"billing_setting"|billing_setting\.billing_'

Repository: QuantumNous/new-api

Length of output: 10524


🏁 Script executed:

# Check how GlobalConfig.Register and LoadFromDB work
find . -name "config.go" -o -name "global_config.go" | head -5

Repository: QuantumNous/new-api

Length of output: 166


🏁 Script executed:

# Check the config package structure
fd -t f "\.go$" setting/ | grep -i config | head -20

Repository: QuantumNous/new-api

Length of output: 160


🏁 Script executed:

# Look at the config/config.go to understand Register and LoadFromDB
fd -t f "config.go" | grep -E "setting/config|config/config"

Repository: QuantumNous/new-api

Length of output: 160


🏁 Script executed:

# Check if expr.md exists in billingexpr
fd -t f "expr.md" pkg/

Repository: QuantumNous/new-api

Length of output: 87


🏁 Script executed:

# Look for types.RWMap mentioned in review
rg -nP "types\.RWMap|RWMap" --max-count=10

Repository: QuantumNous/new-api

Length of output: 2313


🏁 Script executed:

# Read the config.go to understand Register and LoadFromDB implementation
head -100 setting/config/config.go

Repository: QuantumNous/new-api

Length of output: 2051


🏁 Script executed:

# Continue reading config.go
sed -n '100,250p' setting/config/config.go

Repository: QuantumNous/new-api

Length of output: 3565


🏁 Script executed:

# Check the RWMap implementation for comparison
cat types/rw_map.go

Repository: QuantumNous/new-api

Length of output: 2241


🏁 Script executed:

# Check expr.md for concurrency guidance (per learnings)
head -50 pkg/billingexpr/expr.md

Repository: QuantumNous/new-api

Length of output: 2756


🏁 Script executed:

# Check if billing_setting uses RWMap anywhere
rg -n "BillingMode|BillingExpr" setting/billing_setting/ -A 2 -B 2

Repository: QuantumNous/new-api

Length of output: 3647


🏁 Script executed:

# Verify lo.Assign behavior with single map to confirm it does iterate
rg -n "lo.Assign" --max-count=5 -B 2 -A 2

Repository: QuantumNous/new-api

Length of output: 958


🏁 Script executed:

# Check if there's any other mutex/locking on billingSetting in the file
cat setting/billing_setting/tiered_billing.go

Repository: QuantumNous/new-api

Length of output: 3004


Guard billingSetting.BillingMode and BillingExpr maps from concurrent modification during reads.

GetBillingModeCopy and GetBillingExprCopy iterate their source maps via lo.Assign, which is unsafe if another goroutine writes to these maps. Since config.GlobalConfig.LoadFromDB can mutate them at runtime (via updateConfigFromMap), calling both functions in quick succession (as GetPricingSyncData does) raises the likelihood of triggering a "concurrent map iteration and map write" fatal error.

Migrate BillingMode and BillingExpr to types.RWMap[string, string] (following the pattern in setting/ratio_setting/model_ratio.go), or guard them with sync.RWMutex and update all accessors to use the lock.

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

In `@setting/billing_setting/tiered_billing.go` around lines 50 - 67, The current
GetBillingModeCopy and GetBillingExprCopy call lo.Assign on
billingSetting.BillingMode and BillingExpr which can be concurrently written by
config.GlobalConfig.LoadFromDB (via updateConfigFromMap), causing fatal
concurrent map access; fix by protecting these maps either by converting them to
types.RWMap[string,string] (as in setting/ratio_setting/model_ratio.go) and
replacing direct map usage with RWMap.Load/Clone calls in
GetBillingModeCopy/GetBillingExprCopy and callers like GetPricingSyncData, or
add a package-level sync.RWMutex around billingSetting and update all accessors
and writers (including updateConfigFromMap and LoadFromDB paths) to use
RLock/RUnlock for reads and Lock/Unlock for writes so lo.Assign never iterates
an unprotected map.

<ModelRatioNotSetEditor options={inputs} refresh={onRefresh} />
</Tabs.TabPane>
<Tabs.TabPane tab={t('上游倍率同步')} itemKey='upstream_sync'>
<Tabs.TabPane tab={t('上游价格同步')} itemKey='upstream_sync'>

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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm the new key is present and the old key is no longer referenced.
fd -e json . web/src/i18n/locales | xargs -I{} sh -c 'echo ""; echo "==> {}"; jq -r "to_entries | map(select(.key==\"上游价格同步\" or .key==\"上游倍率同步\"))" {}'
echo "---"
echo "Remaining references to the old label:"
rg -nP --type=js --type=jsx --type=ts --type=tsx "上游倍率同步" web/src

Repository: QuantumNous/new-api

Length of output: 417


🏁 Script executed:

#!/bin/bash
# Search for both strings without problematic file type filters
echo "=== Searching for old key '上游倍率同步' ==="
rg "上游倍率同步" web/src

echo ""
echo "=== Searching for new key '上游价格同步' ==="
rg "上游价格同步" web/src

echo ""
echo "=== Checking RatioSetting.jsx around line 109 ==="
wc -l web/src/components/settings/RatioSetting.jsx
head -120 web/src/components/settings/RatioSetting.jsx | tail -20

Repository: QuantumNous/new-api

Length of output: 1621


Update translation files to match the new tab key.

The code change at line 109 replaces the tab label key from 上游倍率同步 to 上游价格同步, but the new key is missing from all locale files (web/src/i18n/locales/{lang}.json). The old key still exists in every locale file, leaving it orphaned.

Add the new translation key 上游价格同步 to all locale files with appropriate translations, and remove the now-unused 上游倍率同步 entries. Run bun run i18n:sync to keep translations in sync.

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

In `@web/src/components/settings/RatioSetting.jsx` at line 109, The tab label key
was changed in RatioSetting.jsx (Tabs.TabPane with itemKey 'upstream_sync') from
'上游倍率同步' to '上游价格同步' but locale JSONs weren’t updated; add the new key '上游价格同步'
with appropriate translations to every locale file in
web/src/i18n/locales/{lang}.json, remove the obsolete '上游倍率同步' entries, and then
run "bun run i18n:sync" to sync translation files.

export const ITEMS_PER_PAGE = 10; // this value must keep same as the one defined in backend!

export const DEFAULT_ENDPOINT = '/api/ratio_config';
export const DEFAULT_ENDPOINT = '/api/pricing';

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

Default endpoint switch will silently retarget channels that didn't pin one.

Previously-configured channels keep their saved endpoint via channelEndpoints merge in UpstreamRatioSync.jsx, so this only affects channels that never had an endpoint set. Those will now hit /api/pricing instead of /api/ratio_config. This is intended for new-api-style upstreams (which expose both), but third-party upstreams that only implement /api/ratio_config will start failing by default after this change. Consider mentioning this in the release notes / UI hint.

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

In `@web/src/constants/common.constant.js` at line 22, Changing DEFAULT_ENDPOINT
to '/api/pricing' will silently break channels that never set an endpoint
because UpstreamRatioSync.jsx only preserves previously-configured endpoints via
the channelEndpoints merge; to fix, either (A) revert DEFAULT_ENDPOINT to
'/api/ratio_config' or (B) implement a safer fallback in the code that resolves
endpoints (e.g., in the logic around channelEndpoints/UpstreamRatioSync.jsx) to
try '/api/ratio_config' if a request to '/api/pricing' returns 404/unsupported,
and add a UI hint or release-note message informing users of the new default;
update references to DEFAULT_ENDPOINT and the endpoint-resolution logic in
UpstreamRatioSync.jsx accordingly.

@Calcium-Ion
Calcium-Ion merged commit f424f90 into QuantumNous:main Apr 26, 2026
1 check passed
isboyjc pushed a commit to isboyjc/amux-api that referenced this pull request Apr 29, 2026
* feat: sync upstream pricing from pricing endpoint

* feat: sync upstream pricing with expression priority

* fix: add feedback while syncing upstream pricing

* fix: show loading state for empty upstream pricing sync
Jinxuans referenced this pull request in TokFlux-Org/TokFlux May 9, 2026
* feat: sync upstream pricing from pricing endpoint

* feat: sync upstream pricing with expression priority

* fix: add feedback while syncing upstream pricing

* fix: show loading state for empty upstream pricing sync
xyfacai pushed a commit to xyfacai/new-api that referenced this pull request May 30, 2026
* feat: sync upstream pricing from pricing endpoint

* feat: sync upstream pricing with expression priority

* fix: add feedback while syncing upstream pricing

* fix: show loading state for empty upstream pricing sync
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.

2 participants