Skip to content

💱 feat(settings): introduce site-wide quota display type - #1957

Merged
Calcium-Ion merged 2 commits into
QuantumNous:mainfrom
seefs001:pr/custom-currency-1923
Oct 3, 2025
Merged

💱 feat(settings): introduce site-wide quota display type#1957
Calcium-Ion merged 2 commits into
QuantumNous:mainfrom
seefs001:pr/custom-currency-1923

Conversation

@seefs001

@seefs001 seefs001 commented Oct 2, 2025

Copy link
Copy Markdown
Collaborator

…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
    DisplayInCurrencyEnabledgeneral_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.

Summary by CodeRabbit

  • New Features
    • Added quota display types: USD, CNY, TOKENS, CUSTOM with configurable symbol and exchange rate.
    • Unified setting to switch display type and edit rate/symbol; persisted in status and local storage.
  • Improvements
    • Usage, billing, top-up, logging, and pricing now respect the selected display type; tokens mode handled consistently.
  • UI
    • Settings revamped with a combined “1 USD = …” rate input; Pricing and header filters include “Custom currency”.
  • Localization
    • Added custom-currency translations (EN/FR).
  • Chores
    • Minor formatting and non-functional refactors.

t0ng7u and others added 2 commits September 29, 2025 23:23
…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
@coderabbitai

coderabbitai Bot commented Oct 2, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Introduces 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

Cohort / File(s) Summary of changes
Quota display settings (backend)
setting/operation_setting/general_setting.go, common/constants.go, model/option.go
Adds quota display constants and public helpers (IsCurrencyDisplay, GetQuotaDisplayType, GetCurrencySymbol, GetUsdToCurrencyRate). Extends GeneralSetting with quota_display_type and custom currency fields. Introduces DisplayInCurrencyEnabled flag (legacy compat) and maps old option to new setting.
Billing and status endpoints
controller/billing.go, controller/misc.go
Switches subscription/usage display logic to operation_setting quota display types (USD/CNY/TOKENS). GetStatus now returns quota_display_type, custom_currency_symbol, custom_currency_exchange_rate; uses IsCurrencyDisplay for backward-compatible flag.
Top-up flows
controller/topup.go, controller/topup_stripe.go
Replaces currency flag checks with quota_display_type. Converts token amounts to USD when display type is TOKENS; adjusts min top-up accordingly.
Logger quota formatting
logger/logger.go
LogQuota and FormatQuota revamped to honor USD, CNY, CUSTOM, and TOKENS using operation_setting; default/fallback handling added.
Settings UI/state (frontend)
web/src/pages/Setting/Operation/SettingsGeneral.jsx, web/src/components/settings/OperationSetting.jsx
Introduces quota_display_type, custom symbol/rate. Replaces separate USDExchangeRate/QuotaPerUnit inputs with combined rate input tied to selected display type. Initializes inputs with new keys and migration from legacy flag.
Rendering and helpers (frontend)
web/src/helpers/render.jsx, web/src/helpers/utils.jsx, web/src/helpers/data.js, web/src/components/table/model-pricing/filter/PricingDisplaySettings.jsx, web/src/components/table/model-pricing/layout/header/SearchActions.jsx, web/src/hooks/model-pricing/useModelPricingData.jsx
Renders quotas per display type including CUSTOM (symbol/rate from status). Stores quota_display_type in localStorage. Adds CUSTOM option to currency pickers. Pricing hooks sync with site display type and custom rates/symbols.
Ollama channel refactors
relay/channel/ollama/adaptor.go, relay/channel/ollama/dto.go, relay/channel/ollama/relay-ollama.go, relay/channel/ollama/stream.go
Formatting, nil-guarding, and small refactors; improved error handling in image processing; no API changes.
i18n updates
web/src/i18n/locales/en.json, web/src/i18n/locales/fr.json
Adds keys for custom currency UI and site display type; removes prior USD exchange rate description entry.
Minor formatting/misc
common/database.go, constant/api_type.go, controller/setup.go, relay/channel/submodel/constants.go, web/index.html, web/src/components/settings/personal/cards/AccountManagement.jsx, web/src/components/table/channels/modals/EditChannelModal.jsx, web/src/components/table/task-logs/modals/ContentModal.jsx
Whitespace/formatting only; no behavioral 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
Loading
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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Possibly related PRs

Suggested reviewers

  • creamlike1024
  • xyfacai

Poem

A nibble of numbers, a hop through rates,
USD, CNY—now custom awaits.
Tokens or tender, I twitch my ear—
Symbols shine crisp, conversion clear.
With every carrot-sized unit I see,
Our quotas align—so hoppily! 🥕

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Title Check ⚠️ Warning The title accurately summarizes the key change—introducing a site-wide quota display type—but includes an emoji, which is considered noise and deviates from the guideline to keep titles concise and free of non-essential characters. Please remove the leading emoji so the title reads, for example, “feat(settings): introduce site-wide quota display type” to comply with concise and noise-free naming conventions.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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/hooks/channels/useChannelsData.jsx (1)

766-838: Fix testingModels collisions across endpoint types.

testingModels still tracks entries by model alone. With the new endpointType argument, 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 a resolveCurrencySymbol(currency) helper. Custom rates are already applied upstream in useModelPricingData.jsx, so this helper only needs to return the correct symbol.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66d0764 and 3199e2e.

📒 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 ollamaChatStreamChunk struct appropriately models the Ollama response format, and toUnix provides 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.EOF

The logic properly handles both Message (chat) and Response (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:

  1. Primary: Parse as newline-delimited JSON (NDJSON) to handle streaming-style responses
  2. 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 Message and Response fields from Ollama API
  • Proper fallbacks for model name and timestamp
  • Uses contentPtr helper for OpenAI compatibility (nil for empty content)

273-278: LGTM: Proper OpenAI API compatibility helper.

The contentPtr helper correctly returns nil for empty strings to maintain OpenAI API compatibility, where absent content should be represented as nil rather 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 QuotaPerUnit correctly 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 QuotaPerUnit to 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 GetSubscription implementation (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 getPayMoney in controller/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 getMinTopup in controller/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 USDExchangeRate multiplication
  • 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 LogQuota implementation (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 DisplayInCurrencyEnabled flag. The naming is consistent and self-documenting.


17-22: LGTM! New fields are well-documented and properly typed.

The three new fields extend the GeneralSetting struct appropriately:

  • QuotaDisplayType: Stores the active display mode
  • CustomCurrencySymbol: Allows custom currency symbols (e.g., "₹", "€")
  • CustomCurrencyExchangeRate: Supports custom exchange rates

The 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 users
  • CustomCurrencySymbol: "¤" - Unicode generic currency sign is appropriate when no custom symbol is set
  • CustomCurrencyExchangeRate: 1.0 - Neutral fallback that won't distort values

These 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_setting with config.GlobalConfig integrates 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() and IsCNYDisplay(): Convenient boolean checks
  • GetQuotaDisplayType(): Simple getter for the active type
  • GetCurrencySymbol(): Type-safe symbol lookup with fallbacks
  • GetUsdToCurrencyRate(): Exchange rate calculation with fallbacks

The fallback logic in GetCurrencySymbol (lines 67-70) and GetUsdToCurrencyRate (lines 84-87) is consistent with the defaults (lines 31-32) and the usage in logger/logger.go.

model/option.go (1)

243-251: Backward compatibility mapping is safe; no direct reads of common.DisplayInCurrencyEnabled found outside initialization.

controller/misc.go (1)

69-86: Status payload adds quota_display_type and custom currency fields — LGTM

Back-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 locales

Keys align with UI usage. Validate JSON and parity with other locales to avoid runtime missing-string issues.

Comment thread common/constants.go
Comment on lines 21 to 24
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
// 保留旧变量以兼容历史逻辑,实际展示由 general_setting.quota_display_type 控制
var DisplayInCurrencyEnabled = true
var DisplayTokenStatEnabled = true

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Comment on lines +135 to 141
<video
src={modalContent}
controls
style={{ width: '100%' }}
autoPlay
crossOrigin="anonymous"
crossOrigin='anonymous'
onError={handleVideoError}

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

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.

Suggested change
<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.

Comment thread web/src/helpers/data.js
Comment on lines +26 to 29
// 兼容:保留旧字段,同时写入新的额度展示类型
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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
// 兼容:保留旧字段,同时写入新的额度展示类型
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.

Comment on lines 904 to 923
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;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +1809 to 1813
"站点额度展示类型及汇率": "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",

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

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
done

Length 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
done

Length 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.

@Calcium-Ion
Calcium-Ion merged commit cc6fceb into QuantumNous:main Oct 3, 2025
1 check passed
@coderabbitai coderabbitai Bot mentioned this pull request Oct 14, 2025
@coderabbitai coderabbitai Bot mentioned this pull request Apr 4, 2026
9 tasks
x22x22 pushed a commit to x22x22/new-api that referenced this pull request Apr 24, 2026
…1923

💱 feat(settings): introduce site-wide quota display type
jiutubaba pushed a commit to jiutubaba/fx-api that referenced this pull request May 17, 2026
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.
@coderabbitai coderabbitai Bot mentioned this pull request May 30, 2026
11 tasks
@coderabbitai coderabbitai Bot mentioned this pull request Jun 24, 2026
11 tasks
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.

3 participants