feat: sync upstream pricing from pricing endpoint - #4452
Conversation
WalkthroughThe upstream synchronization system is expanded from simple ratio syncing to comprehensive pricing and billing sync. The default backend endpoint transitions from Changes
Sequence DiagramsequenceDiagram
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
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: 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 | 🟡 MinorConflict 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, andaudio_completion_ratio, but the conflict description forlocalCat === 'ratio'(Line 484) only formatsModelRatio+CompletionRatio. Likewise, whennewCat === 'ratio'onlymodel_ratio/completion_ratioare shown (Lines 490–492). If a model has e.g. only anImageRatioconfigured locally and an upstream that switches it tomodel_price, the user sees模型倍率: -/补全倍率: -and can't tell what is actually being replaced.Consider iterating
ratioSyncFieldsto 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 — prefervalueMapeverywhere.
cache_ratio,create_cache_ratio,image_ratio,audio_ratio,audio_completion_ratio,billing_mode, andbilling_exprare converted with the newvalueMaphelper, whilemodel_ratio,completion_ratio, andmodel_pricestill hand-roll themap[string]float64 → map[string]anyconversion. 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.
pricingSyncFieldsandnumericPricingSyncFieldscorrectly partition into ratio/price (numeric) andbilling_mode/billing_expr(string). Just be aware future fields must be added to both lists if they're numeric, otherwisenormalizeSyncValuewill pass through the rawjson.Number/typed value andvaluesEqual'sfloat64fast-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: prefermaps.Cloneoverlo.Assignfor a single-source copy.
lo.Assign(billingSetting.BillingMode)works, butmaps.Clone(Go 1.21+) is the idiomatic way to clone a single map and avoids pulling in the variadic merge path. Same applies tomodel_ratio.go's reliance onRWMap.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 inupstreamNamesuseMemo.
useMemo's dependency array (Line 744) lists[filteredDataSource, ratioTypeFilter], butfilteredDataSourceis already derived fromratioTypeFilter, so listing it again is redundant. Either dropratioTypeFilteror rely onfilteredDataSourcealone 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, andshouldShowSyncFieldare recreated on every render. Pure ones (everything exceptgetSyncFieldLabel, which closes overt) can live at module scope. Even the label function can taketas a parameter or be wrapped in auseMemo. 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 intoweb/src/i18n/locales/{lang}.jsonand 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
📒 Files selected for processing (7)
controller/ratio_sync.gosetting/billing_setting/tiered_billing.gosetting/ratio_setting/model_ratio.goweb/src/components/settings/ChannelSelectorModal.jsxweb/src/components/settings/RatioSetting.jsxweb/src/constants/common.constant.jsweb/src/pages/Setting/Ratio/UpstreamRatioSync.jsx
| 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 | ||
| } |
There was a problem hiding this comment.
🧩 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=goRepository: 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 10Repository: 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 3Repository: 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 -80Repository: 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 3Repository: QuantumNous/new-api
Length of output: 588
🏁 Script executed:
# Check the GetPricingSyncData function to understand data flow
rg -nP 'func.*GetPricingSyncData' --type=go -A 20Repository: 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.goRepository: 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.
| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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 alsoratio. - However, if a model previously synced as
tiered_exprupstream is now set back toratioupstream, that "switch back" signal is dropped. The diff will show no change forbilling_mode, so the user has no way to revert through the sync flow — they'd have to manually clear the localbilling_setting.billing_modeentry.
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.
| 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) | ||
| } |
There was a problem hiding this comment.
🧩 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 -5Repository: QuantumNous/new-api
Length of output: 166
🏁 Script executed:
# Check the config package structure
fd -t f "\.go$" setting/ | grep -i config | head -20Repository: 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=10Repository: QuantumNous/new-api
Length of output: 2313
🏁 Script executed:
# Read the config.go to understand Register and LoadFromDB implementation
head -100 setting/config/config.goRepository: QuantumNous/new-api
Length of output: 2051
🏁 Script executed:
# Continue reading config.go
sed -n '100,250p' setting/config/config.goRepository: QuantumNous/new-api
Length of output: 3565
🏁 Script executed:
# Check the RWMap implementation for comparison
cat types/rw_map.goRepository: QuantumNous/new-api
Length of output: 2241
🏁 Script executed:
# Check expr.md for concurrency guidance (per learnings)
head -50 pkg/billingexpr/expr.mdRepository: 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 2Repository: 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 2Repository: 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.goRepository: 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'> |
There was a problem hiding this comment.
🧩 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/srcRepository: 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 -20Repository: 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'; |
There was a problem hiding this comment.
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.
* 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
* 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
* 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
Important
📝 变更描述 / Description
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
上游价格同步支持表达式计费同步,优化UI/UX
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)

Summary by CodeRabbit
Release Notes
New Features
Style