💱 feat(settings): introduce site-wide quota display type - #1957
Conversation
…ENS/CUSTOM)
Replace the legacy boolean “DisplayInCurrencyEnabled” with an injected, type-safe
configuration `general_setting.quota_display_type`, and wire it through the
backend and frontend.
Backend
- Add `QuotaDisplayType` to `operation_setting.GeneralSetting` with injected
registration via `config.GlobalConfig.Register("general_setting", ...)`.
Helpers: `IsCurrencyDisplay()`, `IsCNYDisplay()`, `GetQuotaDisplayType()`.
- Expose `quota_display_type` in `/api/status` and keep legacy
`display_in_currency` for backward compatibility.
- Logger: update `LogQuota` and `FormatQuota` to support USD/CNY/TOKENS. When
CNY is selected, convert using `operation_setting.USDExchangeRate`.
- Controllers:
- `billing`: compute subscription/usage amounts based on the selected type
(USD: divide by `QuotaPerUnit`; CNY: USD→CNY; TOKENS: keep raw tokens).
- `topup` / `topup_stripe`: treat inputs as “amount” for USD/CNY and as
token-count for TOKENS; adjust min topup and pay money accordingly.
- `misc`: include `quota_display_type` in status payload.
- Compatibility: in `model/option.UpdateOption`, map updates to
`DisplayInCurrencyEnabled` → `general_setting.quota_display_type`
(true→USD, false→TOKENS). Keep exporting the legacy key in `OptionMap`.
Frontend
- Settings: replace the “display in currency” switch with a Select
(`general_setting.quota_display_type`) offering USD / CNY / Tokens.
Provide fallback mapping from legacy `DisplayInCurrencyEnabled`.
- Persist `quota_display_type` to localStorage (keep `display_in_currency`
for legacy components).
- Rendering helpers: base all quota/price rendering on `quota_display_type`;
use `usd_exchange_rate` for CNY symbol/values.
- Pricing page: default view currency follows site display type (USD/CNY),
while TOKENS mode still allows per-view currency toggling when needed.
Notes
- No database migrations required.
- Legacy clients remain functional via compatibility fields.
# Conflicts: # web/src/components/settings/personal/cards/AccountManagement.jsx # web/src/components/table/channels/modals/EditChannelModal.jsx # web/src/hooks/channels/useChannelsData.jsx # web/src/hooks/common/useSidebar.js # web/src/i18n/locales/fr.json # web/src/pages/Setting/Operation/SettingsGeneral.jsx
WalkthroughIntroduces a configurable quota display system (USD, CNY, TOKENS, CUSTOM) via operation_setting. Controllers, logger, and frontend rendering now use GetQuotaDisplayType and related helpers. Settings UI adds combined rate input and custom currency fields. Multiple frontend helpers updated; several files receive formatting-only edits. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as Client (Web)
participant API as Controller (misc.GetStatus)
participant Ops as operation_setting
rect rgba(230,240,255,0.4)
API->>Ops: GetQuotaDisplayType()
API->>Ops: GetCurrencySymbol(), GetUsdToCurrencyRate()
API-->>Client: status { quota_display_type, custom_currency_symbol, custom_currency_exchange_rate, ... }
Client->>Client: Store in localStorage (quota_display_type, symbol, rate)
end
Note over Client: Subsequent renders format quotas per display type
sequenceDiagram
autonumber
participant UI as Client (Top-up UI)
participant API as Controller (topup/\*)
participant Ops as operation_setting
participant Cfg as common/constants
UI->>API: Create top-up (amount)
API->>Ops: GetQuotaDisplayType()
alt Display TOKENS
API->>Cfg: Use QuotaPerUnit
API->>API: Convert tokens → USD (amount / QuotaPerUnit)
else Display USD/CNY/CUSTOM
API->>API: Use amount as provided (currency flow)
end
API-->>UI: Payment session/amount
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~30 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate 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. 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/hooks/channels/useChannelsData.jsx (1)
766-838: Fix testingModels collisions across endpoint types.
testingModelsstill tracks entries bymodelalone. With the newendpointTypeargument, running two tests on the same model but different endpoint types leads the first completion to remove the shared key, so the second in-flight test is no longer tracked (spinner/button states flip back to idle while the request is still running, and users can fire duplicate calls). Track the combination (channel, model, endpointType) instead of the bare model.- const testKey = `${record.id}-${model}`; + const baseTestKey = `${record.id}-${model}`; + const testingKey = endpointType + ? `${baseTestKey}-${endpointType}` + : baseTestKey; ... - setTestingModels((prev) => new Set([...prev, model])); + setTestingModels((prev) => new Set([...prev, testingKey])); ... - const { success, message, time } = res.data; + const { success, message, time } = res.data; ... - setModelTestResults((prev) => ({ + setModelTestResults((prev) => ({ ...prev, - [testKey]: { + [baseTestKey]: { ... - const testKey = `${record.id}-${model}`; - setModelTestResults((prev) => ({ + setModelTestResults((prev) => ({ ...prev, - [testKey]: { + [baseTestKey]: { ... - setTestingModels((prev) => { + setTestingModels((prev) => { const newSet = new Set(prev); - newSet.delete(model); + newSet.delete(testingKey); return newSet; });
🧹 Nitpick comments (3)
web/src/components/table/model-pricing/layout/header/SearchActions.jsx (1)
102-113: Added “CUSTOM” currency option — verify end-to-end conversion and dedupe options
- Ensure CUSTOM uses the correct numeric conversion (not just symbol). Confirm displayPrice (or equivalent) applies custom_currency_exchange_rate when currency === 'CUSTOM'.
- Currency options are now defined here and in PricingDisplaySettings.jsx; consider centralizing to a shared constant to avoid drift.
web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx (1)
56-60: “CUSTOM” currency added — keep options in sync and verify conversion path
- Looks consistent with header Select. Please ensure price computation honors custom_currency_exchange_rate when CUSTOM is chosen.
- Recommend extracting currencyItems to a shared constant to keep UI in sync.
web/src/helpers/utils.jsx (1)
649-664: Extract currency symbol resolution into helper
Symbol resolution in utils.jsx and render.jsx is duplicated; extract into aresolveCurrencySymbol(currency)helper. Custom rates are already applied upstream inuseModelPricingData.jsx, so this helper only needs to return the correct symbol.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (31)
common/constants.go(1 hunks)common/database.go(1 hunks)constant/api_type.go(1 hunks)controller/billing.go(3 hunks)controller/misc.go(1 hunks)controller/setup.go(1 hunks)controller/topup.go(3 hunks)controller/topup_stripe.go(2 hunks)logger/logger.go(2 hunks)model/option.go(1 hunks)relay/channel/ollama/adaptor.go(4 hunks)relay/channel/ollama/dto.go(3 hunks)relay/channel/ollama/relay-ollama.go(4 hunks)relay/channel/ollama/stream.go(1 hunks)relay/channel/submodel/constants.go(1 hunks)setting/operation_setting/general_setting.go(2 hunks)web/index.html(1 hunks)web/src/components/settings/OperationSetting.jsx(1 hunks)web/src/components/settings/personal/cards/AccountManagement.jsx(3 hunks)web/src/components/table/channels/modals/EditChannelModal.jsx(7 hunks)web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx(1 hunks)web/src/components/table/model-pricing/layout/header/SearchActions.jsx(1 hunks)web/src/components/table/task-logs/modals/ContentModal.jsx(3 hunks)web/src/helpers/data.js(1 hunks)web/src/helpers/render.jsx(3 hunks)web/src/helpers/utils.jsx(1 hunks)web/src/hooks/channels/useChannelsData.jsx(24 hunks)web/src/hooks/model-pricing/useModelPricingData.jsx(2 hunks)web/src/i18n/locales/en.json(1 hunks)web/src/i18n/locales/fr.json(2 hunks)web/src/pages/Setting/Operation/SettingsGeneral.jsx(5 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the applyModelMapping function transforms the models list by replacing original model names (mapping values) with display names (mapping keys). The database stores this transformed list containing mapped keys. On channel load, data.models contains these mapped display names, making the initialization filter if (data.models.includes(key)) correct.
Applied to files:
web/src/components/table/channels/modals/EditChannelModal.jsx
📚 Learning: 2025-08-27T02:15:25.448Z
Learnt from: AAEE86
PR: QuantumNous/new-api#1658
File: web/src/components/table/channels/modals/EditChannelModal.jsx:555-569
Timestamp: 2025-08-27T02:15:25.448Z
Learning: In EditChannelModal.jsx, the database stores mapped keys (display names) in the models field after applying model mapping transformations. When loading a channel, data.models contains the mapped keys, not the original model names. The filtering logic if (data.models.includes(key)) in the initialization is correct.
Applied to files:
web/src/components/table/channels/modals/EditChannelModal.jsx
🧬 Code graph analysis (18)
logger/logger.go (3)
setting/operation_setting/general_setting.go (5)
GetQuotaDisplayType(55-57)QuotaDisplayTypeCNY(8-8)QuotaDisplayTypeCustom(10-10)GetGeneralSetting(40-42)QuotaDisplayTypeTokens(9-9)common/constants.go (1)
QuotaPerUnit(21-21)setting/operation_setting/payment_setting_old.go (1)
USDExchangeRate(18-18)
web/src/helpers/utils.jsx (2)
web/src/helpers/render.jsx (11)
symbol(843-843)symbol(913-913)symbol(933-933)statusStr(842-842)statusStr(912-912)statusStr(936-936)statusStr(947-947)s(846-846)s(916-916)s(940-940)s(952-952)web/src/hooks/model-pricing/useModelPricingData.jsx (1)
currency(44-44)
relay/channel/ollama/dto.go (1)
dto/claude.go (1)
Thinking(403-406)
controller/billing.go (3)
setting/operation_setting/general_setting.go (3)
GetQuotaDisplayType(55-57)QuotaDisplayTypeCNY(8-8)QuotaDisplayTypeTokens(9-9)common/constants.go (1)
QuotaPerUnit(21-21)setting/operation_setting/payment_setting_old.go (1)
USDExchangeRate(18-18)
web/src/components/settings/personal/cards/AccountManagement.jsx (2)
web/src/components/settings/PersonalSetting.jsx (2)
userState(48-48)status(61-61)web/src/components/auth/LoginForm.jsx (2)
userState(63-63)status(91-94)
controller/misc.go (5)
setting/operation_setting/general_setting.go (3)
IsCurrencyDisplay(45-47)GetQuotaDisplayType(55-57)GetGeneralSetting(40-42)common/constants.go (6)
BatchUpdateEnabled(119-119)DrawingEnabled(25-25)TaskEnabled(26-26)DataExportEnabled(27-27)DataExportDefaultTime(29-29)DefaultCollapseSidebar(30-30)setting/chat.go (1)
Chats(8-30)setting/operation_setting/operation_setting.go (2)
DemoSiteEnabled(5-5)SelfUseModeEnabled(6-6)setting/auto_group.go (1)
DefaultUseAutoGroup(9-9)
web/src/helpers/render.jsx (2)
web/src/helpers/utils.jsx (3)
statusStr(654-654)symbol(649-649)s(656-656)web/src/components/topup/index.jsx (1)
amount(47-47)
web/src/hooks/model-pricing/useModelPricingData.jsx (2)
web/src/components/topup/index.jsx (1)
statusState(44-44)web/src/hooks/common/useSidebar.js (1)
statusState(29-29)
model/option.go (1)
setting/config/config.go (2)
GlobalConfig(18-18)UpdateConfigFromMap(235-237)
controller/topup.go (1)
setting/operation_setting/general_setting.go (2)
GetQuotaDisplayType(55-57)QuotaDisplayTypeTokens(9-9)
web/src/components/table/task-logs/modals/ContentModal.jsx (1)
web/src/hooks/task-logs/useTaskLogsData.js (1)
modalContent(68-68)
controller/topup_stripe.go (1)
setting/operation_setting/general_setting.go (2)
GetQuotaDisplayType(55-57)QuotaDisplayTypeTokens(9-9)
relay/channel/ollama/relay-ollama.go (7)
relay/channel/ollama/dto.go (6)
OllamaTool(22-25)OllamaToolFunction(16-20)OllamaChatMessage(7-14)OllamaToolCall(27-32)OllamaEmbeddingRequest(57-62)OllamaEmbeddingResponse(64-69)dto/openai_request.go (2)
ContentTypeText(386-386)ResponseFormat(13-16)common/json.go (1)
Unmarshal(8-10)dto/embedding.go (1)
EmbeddingRequest(21-32)dto/openai_response.go (3)
Usage(221-234)OpenAIEmbeddingResponseItem(53-57)OpenAIEmbeddingResponse(59-64)types/error.go (3)
NewAPIError(82-90)NewOpenAIError(215-238)ErrorCodeBadResponseBody(68-68)service/http.go (1)
CloseResponseBodyGracefully(14-22)
relay/channel/ollama/stream.go (8)
relay/helper/common.go (6)
Done(92-94)SetEventStreamHeaders(27-41)GenerateStartEmptyResponse(140-156)StringData(67-73)GenerateStopResponse(158-171)GenerateFinalUsageResponse(173-183)relay/common/relay_info.go (1)
RelayInfo(74-121)dto/openai_response.go (7)
Usage(221-234)ChatCompletionsStreamResponse(140-148)ChatCompletionsStreamResponseChoice(79-84)ChatCompletionsStreamResponseChoiceDelta(86-92)ToolCallResponse(120-126)OpenAITextResponse(38-46)OpenAITextResponseChoice(32-36)types/error.go (5)
NewAPIError(82-90)NewOpenAIError(215-238)ErrorCodeBadResponse(67-67)ErrorCodeBadResponseBody(68-68)ErrorCodeReadResponseBodyFailed(65-65)service/http.go (2)
CloseResponseBodyGracefully(14-22)IOCopyBytesGracefully(24-59)common/utils.go (1)
GetUUID(227-231)common/json.go (1)
Marshal(20-22)logger/logger.go (1)
LogError(64-66)
relay/channel/ollama/adaptor.go (5)
relay/common/relay_info.go (1)
RelayInfo(74-121)dto/gemini.go (1)
GeminiChatRequest(12-20)dto/audio.go (1)
AudioRequest(9-15)relay/constant/relay_mode.go (2)
RelayModeEmbeddings(12-12)RelayModeCompletions(11-11)dto/openai_request.go (1)
OpenAIResponsesRequest(780-804)
web/src/pages/Setting/Operation/SettingsGeneral.jsx (1)
web/src/components/settings/OperationSetting.jsx (1)
inputs(32-73)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
web/src/components/table/channels/modals/EditTagModal.jsx (1)
handleInputChange(77-132)
web/src/hooks/channels/useChannelsData.jsx (2)
web/src/helpers/api.js (6)
res(224-224)res(225-225)res(268-268)res(269-269)API(29-37)API(29-37)web/src/helpers/utils.jsx (5)
showError(122-151)showSuccess(157-159)showInfo(161-163)i(468-468)i(480-480)
🔇 Additional comments (36)
relay/channel/ollama/stream.go (5)
3-20: LGTM: Clean import organization.The imports are well-organized following Go conventions (standard library, project packages, external packages) and all appear to be used in the implementation.
22-63: LGTM: Robust timestamp parsing.The
ollamaChatStreamChunkstruct appropriately models the Ollama response format, andtoUnixprovides resilient timestamp parsing with proper fallbacks for various formats and error cases.
65-173: LGTM: Robust stream handling with proper framing.The stream handler correctly implements the OpenAI-compatible streaming protocol:
- Explicit nil checks prevent panics
- Proper event stream header setup
- Line-by-line scanning with JSON parsing and error handling
- Delta frames for content, reasoning, and tool calls
- Final stop, usage, and [DONE] frames on completion
- Scanner error handling excludes expected
io.EOFThe logic properly handles both
Message(chat) andResponse(generate) fields from Ollama, and safely processes reasoning content with null checks.
176-271: LGTM: Comprehensive non-streaming response handling.The non-stream handler implements a robust two-phase parsing strategy:
- Primary: Parse as newline-delimited JSON (NDJSON) to handle streaming-style responses
- Fallback: Parse entire body as single JSON object
Key strengths:
- Aggregates content and reasoning across multiple chunks
- Consistent null/"null" checks for reasoning content (lines 209-211, 229-231)
- Handles both
MessageandResponsefields from Ollama API- Proper fallbacks for model name and timestamp
- Uses
contentPtrhelper for OpenAI compatibility (nil for empty content)
273-278: LGTM: Proper OpenAI API compatibility helper.The
contentPtrhelper correctly returnsnilfor empty strings to maintain OpenAI API compatibility, where absent content should be represented asnilrather than an empty string pointer.web/index.html (1)
13-13: Formatting-only change looks good.Indenting the analytics tag improves consistency without affecting behavior.
relay/channel/ollama/dto.go (5)
8-13: Struct formatting is consistent.Field alignment updates are clean and keep JSON contract intact.
23-24: Tool struct spacing LGTM.Formatting tweak keeps the struct tidy without altering behavior.
46-55: Generate request formatting unchanged semantically.Whitespace adjustments maintain readability with no logic change.
58-61: Embedding request alignment OK.Indentation update maintains clarity.
65-69: Response struct formatting approved.No functional differences; layout is clearer.
web/src/components/settings/personal/cards/AccountManagement.jsx (3)
94-95: Line wrap looks good.Multiline state declaration keeps things readable without changing logic.
240-242: Disabled condition formatting is fine.Line breaks aid readability; behavior stays the same.
399-401: LinuxDO button condition unchanged logically.Formatting tweak is clean and retains intent.
web/src/components/settings/OperationSetting.jsx (1)
45-46: Defaulting to'USD'matches the new setting.Initializing the quota display type as a string aligns with the backend change and keeps the boolean conversion guard working.
web/src/hooks/model-pricing/useModelPricingData.jsx (3)
67-74: LGTM! Sensible defaults for custom currency fields.The fallback values (1 for exchange rate, '¤' for symbol) align with the backend defaults in
setting/operation_setting/general_setting.go(lines 31-32).
77-89: LGTM! Currency synchronization logic is correct.The effect correctly synchronizes the local currency state with the site display type for USD, CNY, and CUSTOM modes, while preserving user-level currency toggling for TOKENS mode. This aligns with the PR objectives.
182-183: LGTM! CUSTOM currency display is correctly implemented.The logic mirrors the CNY conversion path and uses the custom exchange rate and symbol introduced earlier. This completes the support for the new CUSTOM display type.
controller/topup.go (3)
87-94: LGTM! Token-to-USD conversion logic is correct.When the display type is TOKENS, the frontend passes token amounts, so dividing by
QuotaPerUnitcorrectly converts to the USD equivalent for payment processing. The inline comments helpfully explain the intent.
117-124: LGTM! Minimum top-up conversion is correct.When the display type is TOKENS, the minimum top-up threshold is correctly scaled by
QuotaPerUnitto align with the frontend's token-based input.
180-184: LGTM! Top-up record conversion is consistent.The amount conversion here mirrors the logic in
getPayMoney(lines 87-94), ensuring that the stored top-up record uses the correct USD-equivalent amount.controller/billing.go (2)
43-55: LGTM! Subscription amount conversion is correct.The switch statement correctly handles all quota display types:
- USD: Standard conversion via
QuotaPerUnit- CNY: USD conversion followed by exchange rate multiplication
- TOKENS: Raw token count (no conversion)
The inline comments clearly explain the logic, and the implementation aligns with the helpers in
setting/operation_setting/general_setting.go.
94-101: LGTM! Usage amount conversion mirrors subscription logic.The switch statement is consistent with the
GetSubscriptionimplementation (lines 43-55), ensuring that usage and subscription amounts are converted using the same rules.web/src/components/table/channels/modals/EditChannelModal.jsx (1)
410-413: Formatting-only changes throughout the file.The changes in this file are purely cosmetic:
- Multi-line splits for readability (lines 410-413, 935-941, 983-984)
- JSX property formatting with trailing commas (lines 2073-2094)
- String formatting (lines 1443-1445)
No behavioral or logic changes were introduced. These align with the AI summary noting "formatting updates like line splits/joins, trailing commas, expanded multi-line strings."
Also applies to: 935-941, 1443-1445, 2073-2094, 983-984
controller/topup_stripe.go (2)
261-263: LGTM! Token-to-USD conversion is consistent with topup.go.The logic matches
getPayMoneyincontroller/topup.go(lines 87-94), ensuring consistent behavior across payment methods.
282-284: LGTM! Minimum top-up conversion is consistent with topup.go.The logic matches
getMinTopupincontroller/topup.go(lines 117-124), ensuring consistent minimum thresholds across payment methods.logger/logger.go (2)
95-120: LGTM! Quota display logic is comprehensive and handles all types.The switch statement correctly implements the quota display logic for all types:
- USD: Standard conversion via
QuotaPerUnit- CNY: USD conversion followed by
USDExchangeRatemultiplication- CUSTOM: USD conversion followed by custom exchange rate multiplication, with sensible fallbacks (symbol '¤', rate 1)
- TOKENS: Raw token count with no conversion
The fallback handling for CUSTOM aligns with the defaults in
setting/operation_setting/general_setting.go(lines 31-32).
122-146: LGTM! Format logic mirrors LogQuota for consistency.The switch statement mirrors the
LogQuotaimplementation (lines 95-120), ensuring that quota values are formatted consistently across the codebase. The only difference is the absence of the " 额度" suffix, which is appropriate for different display contexts.setting/operation_setting/general_setting.go (5)
5-11: LGTM! Well-defined quota display type constants.The four constants (USD, CNY, TOKENS, CUSTOM) provide a clear, extensible replacement for the previous boolean
DisplayInCurrencyEnabledflag. The naming is consistent and self-documenting.
17-22: LGTM! New fields are well-documented and properly typed.The three new fields extend the
GeneralSettingstruct appropriately:
QuotaDisplayType: Stores the active display modeCustomCurrencySymbol: Allows custom currency symbols (e.g., "₹", "€")CustomCurrencyExchangeRate: Supports custom exchange ratesThe inline comments clearly explain each field's purpose, and the JSON tags ensure proper API serialization.
27-32: LGTM! Sensible default values.The defaults are well-chosen:
QuotaDisplayType: QuotaDisplayTypeUSD- Safe default for international usersCustomCurrencySymbol: "¤"- Unicode generic currency sign is appropriate when no custom symbol is setCustomCurrencyExchangeRate: 1.0- Neutral fallback that won't distort valuesThese defaults align with the fallback logic in
logger/logger.go(lines 107-112, 133-138).
35-37: LGTM! Registration enables centralized config management.Registering
general_settingwithconfig.GlobalConfigintegrates the new settings into the centralized configuration system, allowing them to be updated dynamically via the config management API. This is consistent with the pattern used throughout the codebase.
44-91: LGTM! Public API is well-designed and consistent.The five new functions provide a clean, type-safe interface for working with quota display types:
IsCurrencyDisplay()andIsCNYDisplay(): Convenient boolean checksGetQuotaDisplayType(): Simple getter for the active typeGetCurrencySymbol(): Type-safe symbol lookup with fallbacksGetUsdToCurrencyRate(): Exchange rate calculation with fallbacksThe fallback logic in
GetCurrencySymbol(lines 67-70) andGetUsdToCurrencyRate(lines 84-87) is consistent with the defaults (lines 31-32) and the usage inlogger/logger.go.model/option.go (1)
243-251: Backward compatibility mapping is safe; no direct reads ofcommon.DisplayInCurrencyEnabledfound outside initialization.controller/misc.go (1)
69-86: Status payload adds quota_display_type and custom currency fields — LGTMBack-compat via display_in_currency retained, with new type and custom fields exposed for modern clients. Please confirm the frontend persists these fields in localStorage and uses them in rendering.
web/src/i18n/locales/en.json (1)
1813-1817: Added EN translations for custom currency — ensure consistency across localesKeys align with UI usage. Validate JSON and parity with other locales to avoid runtime missing-string issues.
| var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens | ||
| // 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制 | ||
| var DisplayInCurrencyEnabled = true | ||
| var DisplayTokenStatEnabled = true |
There was a problem hiding this comment.
Keep legacy flag in sync with the new display type.
We’re defaulting DisplayInCurrencyEnabled to true, but any remaining callers relying on this flag (or the legacy status payload) will now get stale data when quota_display_type is TOKENS. Please ensure this variable is updated alongside general_setting.quota_display_type, or derive it on demand so legacy consumers still receive accurate information.
🤖 Prompt for AI Agents
In common/constants.go around lines 21 to 24, the legacy flag
DisplayInCurrencyEnabled is hard-coded true and can drift from
general_setting.quota_display_type; change usage so the legacy flag is kept in
sync by either (1) removing the hard-coded variable and replacing callers to
derive the value on demand from the current general_setting.quota_display_type
(recommended), or (2) if keeping the variable, update it whenever
general_setting.quota_display_type is set/loaded (e.g., set
DisplayInCurrencyEnabled = (quota_display_type != "TOKENS") after config load)
so legacy consumers always see accurate state.
| <video | ||
| src={modalContent} | ||
| controls | ||
| style={{ width: '100%' }} | ||
| autoPlay | ||
| crossOrigin="anonymous" | ||
| crossOrigin='anonymous' | ||
| onError={handleVideoError} |
There was a problem hiding this comment.
Revert forced CORS on the video element.
Adding crossOrigin='anonymous' forces a CORS fetch; any third-party video URL without the proper Access-Control-Allow-Origin header will now fail to load where it previously worked, regressing playback for many providers. Drop this attribute (or make it conditional on confirmed CORS support).
Apply this diff to remove the regression:
- <video
- src={modalContent}
- controls
- style={{ width: '100%' }}
- autoPlay
- crossOrigin='anonymous'
+ <video
+ src={modalContent}
+ controls
+ style={{ width: '100%' }}
+ autoPlay📝 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.
| <video | |
| src={modalContent} | |
| controls | |
| style={{ width: '100%' }} | |
| autoPlay | |
| crossOrigin="anonymous" | |
| crossOrigin='anonymous' | |
| onError={handleVideoError} | |
| <video | |
| src={modalContent} | |
| controls | |
| style={{ width: '100%' }} | |
| autoPlay | |
| onError={handleVideoError} |
🤖 Prompt for AI Agents
In web/src/components/table/task-logs/modals/ContentModal.jsx around lines 135
to 141, the video element unconditionally sets crossOrigin='anonymous' which
forces a CORS fetch and breaks playback for third‑party URLs without CORS
headers; remove the crossOrigin attribute (or make it conditional) so the video
can load using default browser behavior for non‑CORS sources, e.g. drop
crossOrigin='anonymous' from the <video> props or add a runtime check to only
set it when the source is known to support CORS.
| // 兼容:保留旧字段,同时写入新的额度展示类型 | ||
| localStorage.setItem('display_in_currency', data.display_in_currency); | ||
| localStorage.setItem('quota_display_type', data.quota_display_type || 'USD'); | ||
| localStorage.setItem('enable_drawing', data.enable_drawing); |
There was a problem hiding this comment.
Fallback should respect legacy display_in_currency.
When hitting an older backend that only returns display_in_currency=false, this code will write quota_display_type="USD" and mis-render quotas that used to stay in tokens. Please derive the fallback from the boolean (true→USD, false→TOKENS) before defaulting to USD.
Consider:
- localStorage.setItem('quota_display_type', data.quota_display_type || 'USD');
+ let quotaDisplayType = data.quota_display_type;
+ if (!quotaDisplayType && typeof data.display_in_currency === 'boolean') {
+ quotaDisplayType = data.display_in_currency ? 'USD' : 'TOKENS';
+ }
+ localStorage.setItem('quota_display_type', quotaDisplayType || 'USD');📝 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.
| // 兼容:保留旧字段,同时写入新的额度展示类型 | |
| localStorage.setItem('display_in_currency', data.display_in_currency); | |
| localStorage.setItem('quota_display_type', data.quota_display_type || 'USD'); | |
| localStorage.setItem('enable_drawing', data.enable_drawing); | |
| // 兼容:保留旧字段,同时写入新的额度展示类型 | |
| localStorage.setItem('display_in_currency', data.display_in_currency); | |
| let quotaDisplayType = data.quota_display_type; | |
| if (!quotaDisplayType && typeof data.display_in_currency === 'boolean') { | |
| quotaDisplayType = data.display_in_currency ? 'USD' : 'TOKENS'; | |
| } | |
| localStorage.setItem('quota_display_type', quotaDisplayType || 'USD'); | |
| localStorage.setItem('enable_drawing', data.enable_drawing); |
🤖 Prompt for AI Agents
In web/src/helpers/data.js around lines 26 to 29, the fallback for
quota_display_type currently unconditionally defaults to 'USD' and ignores older
backends that only return display_in_currency=false; change the logic so that
when data.quota_display_type is absent you derive the value from
data.display_in_currency (map true → 'USD', false → 'TOKENS'), and only use
'USD' as a last-resort default when neither field is provided; then write that
computed value to localStorage.quota_display_type while still preserving
localStorage.display_in_currency and enable_drawing.
| export function renderQuotaWithAmount(amount) { | ||
| let displayInCurrency = localStorage.getItem('display_in_currency'); | ||
| displayInCurrency = displayInCurrency === 'true'; | ||
| if (displayInCurrency) { | ||
| return '$' + amount; | ||
| } else { | ||
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; | ||
| if (quotaDisplayType === 'TOKENS') { | ||
| return renderNumber(renderUnitWithQuota(amount)); | ||
| } | ||
| if (quotaDisplayType === 'CNY') { | ||
| return '¥' + amount; | ||
| } else if (quotaDisplayType === 'CUSTOM') { | ||
| const statusStr = localStorage.getItem('status'); | ||
| let symbol = '¤'; | ||
| try { | ||
| if (statusStr) { | ||
| const s = JSON.parse(statusStr); | ||
| symbol = s?.custom_currency_symbol || symbol; | ||
| } | ||
| } catch (e) {} | ||
| return symbol + amount; | ||
| } | ||
| return '$' + amount; | ||
| } |
There was a problem hiding this comment.
Convert USD amounts before labeling as CNY/custom
Line [911] still returns the USD-denominated value with a different currency symbol. Because renderQuotaWithAmount receives USD amounts (the TOKENS branch multiplies by quota_per_unit to convert), the CNY and CUSTOM paths must apply the configured exchange rate or the UI will misstate every charge when those display types are active. Please multiply by the stored rate and format the result before prefixing the symbol.
if (quotaDisplayType === 'TOKENS') {
return renderNumber(renderUnitWithQuota(amount));
}
- if (quotaDisplayType === 'CNY') {
- return '¥' + amount;
- } else if (quotaDisplayType === 'CUSTOM') {
- const statusStr = localStorage.getItem('status');
- let symbol = '¤';
- try {
- if (statusStr) {
- const s = JSON.parse(statusStr);
- symbol = s?.custom_currency_symbol || symbol;
- }
- } catch (e) {}
- return symbol + amount;
- }
+ if (quotaDisplayType === 'CNY') {
+ const statusStr = localStorage.getItem('status');
+ let rate = 1;
+ try {
+ if (statusStr) {
+ const s = JSON.parse(statusStr);
+ rate = parseFloat(s?.usd_exchange_rate) || rate;
+ }
+ } catch (e) {}
+ const hasAmount =
+ amount !== '' && amount !== null && amount !== undefined;
+ const numericAmount = hasAmount ? Number(amount) : NaN;
+ const decimals =
+ typeof amount === 'string' && amount.includes('.')
+ ? amount.split('.')[1].length
+ : 2;
+ if (Number.isFinite(numericAmount)) {
+ return '¥' + (numericAmount * rate).toFixed(decimals);
+ }
+ return '¥' + amount;
+ } else if (quotaDisplayType === 'CUSTOM') {
+ const statusStr = localStorage.getItem('status');
+ let symbol = '¤';
+ let rate = 1;
+ try {
+ if (statusStr) {
+ const s = JSON.parse(statusStr);
+ symbol = s?.custom_currency_symbol || symbol;
+ rate = parseFloat(s?.custom_currency_exchange_rate) || rate;
+ }
+ } catch (e) {}
+ const hasAmount =
+ amount !== '' && amount !== null && amount !== undefined;
+ const numericAmount = hasAmount ? Number(amount) : NaN;
+ const decimals =
+ typeof amount === 'string' && amount.includes('.')
+ ? amount.split('.')[1].length
+ : 2;
+ if (Number.isFinite(numericAmount)) {
+ return symbol + (numericAmount * rate).toFixed(decimals);
+ }
+ return symbol + amount;
+ }
return '$' + amount;
}📝 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.
| export function renderQuotaWithAmount(amount) { | |
| let displayInCurrency = localStorage.getItem('display_in_currency'); | |
| displayInCurrency = displayInCurrency === 'true'; | |
| if (displayInCurrency) { | |
| return '$' + amount; | |
| } else { | |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; | |
| if (quotaDisplayType === 'TOKENS') { | |
| return renderNumber(renderUnitWithQuota(amount)); | |
| } | |
| if (quotaDisplayType === 'CNY') { | |
| return '¥' + amount; | |
| } else if (quotaDisplayType === 'CUSTOM') { | |
| const statusStr = localStorage.getItem('status'); | |
| let symbol = '¤'; | |
| try { | |
| if (statusStr) { | |
| const s = JSON.parse(statusStr); | |
| symbol = s?.custom_currency_symbol || symbol; | |
| } | |
| } catch (e) {} | |
| return symbol + amount; | |
| } | |
| return '$' + amount; | |
| } | |
| export function renderQuotaWithAmount(amount) { | |
| const quotaDisplayType = localStorage.getItem('quota_display_type') || 'USD'; | |
| if (quotaDisplayType === 'TOKENS') { | |
| return renderNumber(renderUnitWithQuota(amount)); | |
| } | |
| if (quotaDisplayType === 'CNY') { | |
| const statusStr = localStorage.getItem('status'); | |
| let rate = 1; | |
| try { | |
| if (statusStr) { | |
| const s = JSON.parse(statusStr); | |
| rate = parseFloat(s?.usd_exchange_rate) || rate; | |
| } | |
| } catch (e) {} | |
| const hasAmount = | |
| amount !== '' && amount !== null && amount !== undefined; | |
| const numericAmount = hasAmount ? Number(amount) : NaN; | |
| const decimals = | |
| typeof amount === 'string' && amount.includes('.') | |
| ? amount.split('.')[1].length | |
| : 2; | |
| if (Number.isFinite(numericAmount)) { | |
| return '¥' + (numericAmount * rate).toFixed(decimals); | |
| } | |
| return '¥' + amount; | |
| } else if (quotaDisplayType === 'CUSTOM') { | |
| const statusStr = localStorage.getItem('status'); | |
| let symbol = '¤'; | |
| let rate = 1; | |
| try { | |
| if (statusStr) { | |
| const s = JSON.parse(statusStr); | |
| symbol = s?.custom_currency_symbol || symbol; | |
| rate = parseFloat(s?.custom_currency_exchange_rate) || rate; | |
| } | |
| } catch (e) {} | |
| const hasAmount = | |
| amount !== '' && amount !== null && amount !== undefined; | |
| const numericAmount = hasAmount ? Number(amount) : NaN; | |
| const decimals = | |
| typeof amount === 'string' && amount.includes('.') | |
| ? amount.split('.')[1].length | |
| : 2; | |
| if (Number.isFinite(numericAmount)) { | |
| return symbol + (numericAmount * rate).toFixed(decimals); | |
| } | |
| return symbol + amount; | |
| } | |
| return '$' + amount; | |
| } |
🤖 Prompt for AI Agents
In web/src/helpers/render.jsx around lines 904 to 923, the CNY and CUSTOM
branches currently just prefix the USD amount with a different symbol; instead
read the configured exchange rate from localStorage (e.g. a stored rate key used
elsewhere in the app), parse it to a number defaulting to 1, multiply the
incoming USD amount by that rate, format the converted value with the same
number-formatting helper you use for USD (e.g. renderNumber or
renderUnitWithQuota as appropriate, keeping consistent decimal places), then
prefix with the correct symbol (¥ or custom_symbol) and return that string.
| "站点额度展示类型及汇率": "Type d'affichage du quota du site et taux de change", | ||
| "自定义货币": "Devise personnalisée", | ||
| "自定义货币符号": "Symbole de devise personnalisé", | ||
| "例如 €, £, Rp, ₩, ₹...": "Par exemple, €, £, Rp, ₩, ₹...", | ||
| "美元汇率": "Taux de change USD", |
There was a problem hiding this comment.
🧩 Analysis chain
New i18n keys for custom currency added — validate locale parity
Translations look good. Please ensure these keys exist and are valid across all locales.
🏁 Script executed:
#!/bin/bash
# Validate JSON and presence of new keys in en/fr locales
set -e
for f in web/src/i18n/locales/en.json web/src/i18n/locales/fr.json; do
echo "Validating $f"
cat "$f" | jq -e . >/dev/null
done
echo "Searching for required keys:"
for k in '自定义货币' '自定义货币符号' '例如 €, £, Rp, ₩, ₹...' '站点额度展示类型及汇率'; do
echo "Key: $k"
rg -n "$k" web/src/i18n/locales/en.json web/src/i18n/locales/fr.json || true
doneLength of output: 1006
🏁 Script executed:
#!/bin/bash
for f in web/src/i18n/locales/*.json; do
echo "Checking $f"
for k in '自定义货币' '自定义货币符号' '例如 €, £, Rp, ₩, ₹...' '站点额度展示类型及汇率'; do
if rg -q "\"$k\"" "$f"; then
echo " ✔ $k"
else
echo " ✖ Missing $k"
fi
done
doneLength of output: 334
Add missing i18n keys to zh.json
The following keys are missing in web/src/i18n/locales/zh.json:
- 自定义货币
- 自定义货币符号
- 例如 €, £, Rp, ₩, ₹…
- 站点额度展示类型及汇率
🤖 Prompt for AI Agents
In web/src/i18n/locales/zh.json (add near the corresponding quota/currency
section), the four keys present in fr.json are missing in zh.json: "自定义货币",
"自定义货币符号", "例如 €, £, Rp, ₩, ₹...", and "站点额度展示类型及汇率". Add these keys to zh.json
with appropriate Chinese values (they can be the same as the keys if they
represent Chinese labels) so the zh locale includes entries for each string;
place them in the same logical section and run a quick JSON lint to ensure valid
syntax.
…1923 💱 feat(settings): introduce site-wide quota display type
Closes QuantumNous#1957 The OAuth path forwards client requests to chatgpt.com/backend-api/codex/responses, where applyCodexOAuthTransform forces store=false (chatgpt.com's codex backend rejects store=true). Reasoning items emitted under store=false are NEVER persisted upstream, so any rs_* reference that a client carries forward in a subsequent input[] array triggers a guaranteed upstream 404: Item with id 'rs_...' not found. Items are not persisted when `store` is set to false. Try again with `store` set to true, or remove this item from your input. sub2api wraps this as 502 "Upstream request failed" and the conversation breaks on every multi-turn /v1/responses request that uses reasoning + tools (reproducible with gpt-5.5; gpt-5.4 happens to dodge it because the upstream does not emit reasoning items for that model). Affected clients include any that follow the OpenAI Responses API spec and replay prior assistant items verbatim — in practice this hit OpenClaw and similar agent harnesses on every turn ≥2 with tool use. The fix: in filterCodexInput, drop input items with type == "reasoning" entirely. The model never reads reasoning summary text from input (only encrypted_content can carry reasoning context across turns, and chatgpt.com under store=false does not emit it), so this is a no-op for the model itself and a clean removal of unreachable upstream lookups. Scope is intentionally narrow: * Only OAuth account requests (account.Type == AccountTypeOAuth) reach applyCodexOAuthTransform / filterCodexInput. * API-key accounts going to api.openai.com/v1/responses are unaffected (store=true works there, rs_* persists, multi-turn already works). * Anthropic / Gemini platform groups go through different transforms and are unaffected. * /v1/chat/completions is unaffected (no reasoning items). * item_reference items (different type) are unaffected — only type == "reasoning" is dropped. Verification: * Existing tests pass: go test ./internal/service/ -run Codex|Tool|OAuth * New regression test asserts reasoning items are dropped under both preserveReferences=true and preserveReferences=false. * End-to-end repro on gpt-5.5 multi-turn + tools: pre-patch 502, post-patch 200. Repro on gpt-5.4 unchanged. Three-turn deep loop on gpt-5.5 passes.
…ENS/CUSTOM)
Replace the legacy boolean “DisplayInCurrencyEnabled” with an injected, type-safe
configuration
general_setting.quota_display_type, and wire it through thebackend and frontend.
Backend
QuotaDisplayTypetooperation_setting.GeneralSettingwith injectedregistration via
config.GlobalConfig.Register("general_setting", ...).Helpers:
IsCurrencyDisplay(),IsCNYDisplay(),GetQuotaDisplayType().quota_display_typein/api/statusand keep legacydisplay_in_currencyfor backward compatibility.LogQuotaandFormatQuotato support USD/CNY/TOKENS. WhenCNY is selected, convert using
operation_setting.USDExchangeRate.billing: compute subscription/usage amounts based on the selected type(USD: divide by
QuotaPerUnit; CNY: USD→CNY; TOKENS: keep raw tokens).topup/topup_stripe: treat inputs as “amount” for USD/CNY and astoken-count for TOKENS; adjust min topup and pay money accordingly.
misc: includequota_display_typein status payload.model/option.UpdateOption, map updates toDisplayInCurrencyEnabled→general_setting.quota_display_type(true→USD, false→TOKENS). Keep exporting the legacy key in
OptionMap.Frontend
(
general_setting.quota_display_type) offering USD / CNY / Tokens.Provide fallback mapping from legacy
DisplayInCurrencyEnabled.quota_display_typeto localStorage (keepdisplay_in_currencyfor legacy components).
quota_display_type;use
usd_exchange_ratefor CNY symbol/values.while TOKENS mode still allows per-view currency toggling when needed.
Notes
Summary by CodeRabbit