Skip to content

feat: support unlimited balance for users (#5827) - #5830

Closed
Gravirei wants to merge 2 commits into
QuantumNous:mainfrom
Gravirei:feat/enhancement
Closed

feat: support unlimited balance for users (#5827)#5830
Gravirei wants to merge 2 commits into
QuantumNous:mainfrom
Gravirei:feat/enhancement

Conversation

@Gravirei

@Gravirei Gravirei commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

📝 变更描述 / Description

Adds an unlimited_balance boolean field to the User model, mirroring the existing Token.UnlimitedQuota pattern. When enabled, all user balance checks during API relay are bypassed, allowing unlimited usage without manually setting a large balance.

How it works

  • Model: User.UnlimitedBalance (bool) — stored in DB, cached in UserBase
  • Propagation: DB → UserBase → Gin context → RelayInfo.UserUnlimitedBalance
  • Balance checks bypassed in all 3 paths:
    • BillingSession.tryWallet() — skips quota ≤ 0 and pre-consume checks
    • PreConsumeQuota() (legacy) — same bypass
    • PreWssConsumeQuota() (realtime) — skips user quota check
  • Trust bypass: Users with unlimited balance are always trusted (pre-consumption skipped)
  • API: Managed via ManageUser endpoint with action "unlimited_balance" + value 0/1
  • UI: Toggle switch in user edit drawer; "Unlimited" badge in users table quota column

🚀 变更类型 / Type of change

  • ✨ 新功能 (New feature)

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

  • 人工确认: I have reviewed and written this description.
  • 非重复提交: Checked existing Issues/PRs — not a duplicate.
  • 变更理解: Understood the full propagation chain and all bypass paths.
  • 范围聚焦: Only files related to this feature are changed (18 files).
  • 本地验证: Code reviewed for correctness; build could not be run due to missing tooling in the environment (no Go/tsgo installed).
  • 安全合规: No secrets; follows project conventions (GORM, common.JSON wrappers, no DB-specific SQL).

Summary by CodeRabbit

  • New Features
    • Added an “Unlimited Balance” user setting that can be toggled from the user management screen.
    • When enabled, the quota UI shows “Unlimited” and the update dialog reflects the unlimited state.
  • Bug Fixes
    • Updated quota/billing enforcement so unlimited-balance users bypass insufficient-quota checks and proceed without being blocked by pre-consumption validation.
  • Documentation
    • Added i18n labels and error/success messaging for unlimited balance (including enabled/disabled states) across supported languages.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 522e5866-fc0d-4264-915a-3c68b4e6d858

📥 Commits

Reviewing files that changed from the base of the PR and between fe814fb and 2600bc0.

📒 Files selected for processing (1)
  • web/default/src/features/users/components/users-mutate-drawer.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • web/default/src/features/users/components/users-mutate-drawer.tsx

Walkthrough

Adds a per-user unlimited-balance flag through backend user data, relay context, quota handling, and user management, plus frontend display/toggle support and locale strings.

Changes

Unlimited Balance Feature

Layer / File(s) Summary
User model, cache, and context key
constant/context_key.go, model/user.go, model/user_cache.go
Adds ContextKeyUserUnlimitedBalance and UnlimitedBalance fields to user and cache models, and propagates the flag through base-user conversion and Gin context writes.
Relay info and quota/billing bypass logic
relay/common/relay_info.go, service/billing_session.go, service/pre_consume_quota.go, service/quota.go
Populates RelayInfo.UserUnlimitedBalance from context and uses it to skip quota-insufficient checks in billing and quota consumption paths.
ManageUser unlimited_balance action
controller/user.go
Adds a "unlimited_balance" action branch that updates the user, invalidates cache, records an audit entry, and returns success.
Frontend types, API, and UI
web/default/src/features/users/types.ts, web/default/src/features/users/api.ts, web/default/src/features/users/components/users-columns.tsx, web/default/src/features/users/components/users-mutate-drawer.tsx
Extends user types, adds the management API call, shows unlimited balance in the users table, and adds an update toggle in the user drawer.
Locale translations
web/default/src/i18n/locales/{en,fr,ja,ru,vi,zh}.json
Adds translation keys for enabling unlimited balance, update failures, and enabled/disabled labels.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant UsersMutateDrawer
  participant manageUserUnlimitedBalance
  participant ManageUser
  participant Cache
  User->>UsersMutateDrawer: toggle unlimited balance
  UsersMutateDrawer->>manageUserUnlimitedBalance: call with id, enabled
  manageUserUnlimitedBalance->>ManageUser: POST /api/user/manage action unlimited_balance
  ManageUser->>Cache: invalidate user cache
  ManageUser-->>manageUserUnlimitedBalance: success response
  manageUserUnlimitedBalance-->>UsersMutateDrawer: refresh user data
Loading
sequenceDiagram
  participant Relay
  participant RelayInfo
  participant BillingSession
  participant PreConsumeQuota
  participant QuotaService
  Relay->>RelayInfo: read ContextKeyUserUnlimitedBalance
  RelayInfo->>BillingSession: pass UserUnlimitedBalance flag
  BillingSession->>BillingSession: shouldTrust returns true for wallet
  BillingSession->>PreConsumeQuota: continue with unlimited balance
  PreConsumeQuota->>PreConsumeQuota: skip insufficient quota checks
  PreConsumeQuota->>QuotaService: trust branch with unlimited balance
  QuotaService->>QuotaService: skip user quota error
Loading

Possibly related PRs

Suggested reviewers: seefs001

Poem

I hop through caches, soft and light,
Unlimited balance feels just right.
A toggle flips, the badges gleam,
Quota checks now drift downstream.
🐰✨ No more counting crumbs for me!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main feature: adding unlimited balance support for users.
Linked Issues check ✅ Passed The PR implements unlimited user balance, propagates it through relay/billing, and adds management/UI support as requested in #5827.
Out of Scope Changes check ✅ Passed The translation updates and UI/backend changes all support the unlimited-balance feature, with no unrelated scope evident.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Gravirei
Gravirei force-pushed the feat/enhancement branch from 2319b35 to fe814fb Compare July 1, 2026 03:19

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

🧹 Nitpick comments (3)
web/default/src/features/users/components/users-mutate-drawer.tsx (3)

405-418: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Nested ternary exceeds one level.

The value expression nests a second ternary (tokensOnly ? ... : ...) inside the unlimited_balance ternary, creating two levels of nesting.

♻️ Suggested refactor: extract a small helper
+  const getQuotaInputValue = () => {
+    if (currentRow?.unlimited_balance) return t('Unlimited')
+    if (tokensOnly) return String(field.value || 0)
+    return (field.value || 0).toFixed(6)
+  }
+
   <Input
-    value={
-      currentRow?.unlimited_balance
-        ? t('Unlimited')
-        : tokensOnly
-          ? String(field.value || 0)
-          : (field.value || 0).toFixed(6)
-    }
+    value={getQuotaInputValue()}
     readOnly
     className='flex-1'
   />

As per path instructions, "Avoid nested ternary expressions deeper than one level; prefer if/else, early returns, or extracted helper functions."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/users/components/users-mutate-drawer.tsx` around
lines 405 - 418, The Input value expression in users-mutate-drawer.tsx uses a
nested ternary inside the currentRow?.unlimited_balance check, which violates
the no-deep-nesting rule. Refactor the value logic by extracting it into a small
helper or computing it with if/else/early returns near the affected JSX in the
users-mutate-drawer component, so the Unlimited, tokensOnly, and fixed-precision
cases are handled without nested ternaries.

Source: Path instructions


438-477: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Switch label/description aren't programmatically associated with the control.

Unlike the surrounding FormField-wrapped controls (which get htmlFor/aria-* wiring via useFormField()), this FormLabel/FormDescription pair sits outside any FormItem/FormField context, so they render as plain text with no htmlFor/id link to the Switch. Screen reader users won't get an accessible name for the toggle.

♿ Suggested fix: wire up explicit ids
-                    <div className={sideDrawerSwitchItemClassName()}>
+                    <div className={sideDrawerSwitchItemClassName()}>
                       <div className='flex flex-col gap-0.5'>
-                        <FormLabel className='text-sm'>
+                        <FormLabel htmlFor='unlimited-balance-switch' className='text-sm'>
                           {t('Unlimited Balance')}
                         </FormLabel>
-                        <FormDescription className='text-xs'>
+                        <FormDescription id='unlimited-balance-desc' className='text-xs'>
                           {t('Enable unlimited balance for this user')}
                         </FormDescription>
                       </div>
                       <Switch
+                        id='unlimited-balance-switch'
+                        aria-describedby='unlimited-balance-desc'
                         checked={currentRow.unlimited_balance ?? false}

As per path instructions, "Use semantic HTML, proper form label associations, keyboard accessibility, appropriate ARIA attributes."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/users/components/users-mutate-drawer.tsx` around
lines 438 - 477, The Unlimited Balance toggle in users-mutate-drawer.tsx is
missing a programmatic label association, so the FormLabel/FormDescription are
not tied to the Switch. Update the toggle block around the
currentRow.unlimited_balance Switch to give the control a stable id and connect
the text with explicit htmlFor and/or aria-labelledby/aria-describedby wiring.
Keep the existing FormLabel and FormDescription content, but make sure the
Switch has an accessible name and description for screen readers.

Source: Path instructions


449-475: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Switch isn't disabled while the request is in flight.

Rapid toggling before manageUserUnlimitedBalance resolves can fire overlapping requests, and the UI gives no feedback that a change is pending (only relies on currentRow being refreshed afterward, which may lag a re-render).

🔒 Suggested fix: track a pending flag
+  const [isTogglingBalance, setIsTogglingBalance] = useState(false)
...
                       <Switch
                        checked={currentRow.unlimited_balance ?? false}
+                        disabled={isTogglingBalance}
                         onCheckedChange={async (checked) => {
+                          setIsTogglingBalance(true)
                           try {
                             const result = await manageUserUnlimitedBalance(
                               currentRow.id,
                               checked
                             )
                             ...
                           } catch {
                             toast.error(t('Failed to update unlimited balance'))
+                          } finally {
+                            setIsTogglingBalance(false)
                           }
                         }}
                       />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/default/src/features/users/components/users-mutate-drawer.tsx` around
lines 449 - 475, The unlimited-balance Switch in users-mutate-drawer.tsx is not
protected against overlapping updates, so rapid toggles can trigger multiple
manageUserUnlimitedBalance calls before the first resolves. Add a
pending/loading state around the Switch (for example in the currentRow update
handler near manageUserUnlimitedBalance and refreshUserData) that is set before
awaiting the request and cleared in finally, then pass it to the Switch as
disabled and use it to block further onCheckedChange calls until the request
finishes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@web/default/src/features/users/components/users-mutate-drawer.tsx`:
- Around line 405-418: The Input value expression in users-mutate-drawer.tsx
uses a nested ternary inside the currentRow?.unlimited_balance check, which
violates the no-deep-nesting rule. Refactor the value logic by extracting it
into a small helper or computing it with if/else/early returns near the affected
JSX in the users-mutate-drawer component, so the Unlimited, tokensOnly, and
fixed-precision cases are handled without nested ternaries.
- Around line 438-477: The Unlimited Balance toggle in users-mutate-drawer.tsx
is missing a programmatic label association, so the FormLabel/FormDescription
are not tied to the Switch. Update the toggle block around the
currentRow.unlimited_balance Switch to give the control a stable id and connect
the text with explicit htmlFor and/or aria-labelledby/aria-describedby wiring.
Keep the existing FormLabel and FormDescription content, but make sure the
Switch has an accessible name and description for screen readers.
- Around line 449-475: The unlimited-balance Switch in users-mutate-drawer.tsx
is not protected against overlapping updates, so rapid toggles can trigger
multiple manageUserUnlimitedBalance calls before the first resolves. Add a
pending/loading state around the Switch (for example in the currentRow update
handler near manageUserUnlimitedBalance and refreshUserData) that is set before
awaiting the request and cleared in finally, then pass it to the Switch as
disabled and use it to block further onCheckedChange calls until the request
finishes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4c69b95-77f6-47fb-ab1b-149aae282cbe

📥 Commits

Reviewing files that changed from the base of the PR and between 5bf3468 and fe814fb.

📒 Files selected for processing (18)
  • constant/context_key.go
  • controller/user.go
  • model/user.go
  • model/user_cache.go
  • relay/common/relay_info.go
  • service/billing_session.go
  • service/pre_consume_quota.go
  • service/quota.go
  • web/default/src/features/users/api.ts
  • web/default/src/features/users/components/users-columns.tsx
  • web/default/src/features/users/components/users-mutate-drawer.tsx
  • web/default/src/features/users/types.ts
  • web/default/src/i18n/locales/en.json
  • web/default/src/i18n/locales/fr.json
  • web/default/src/i18n/locales/ja.json
  • web/default/src/i18n/locales/ru.json
  • web/default/src/i18n/locales/vi.json
  • web/default/src/i18n/locales/zh.json

@Gravirei Gravirei closed this Jul 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] 自用模式下支持将用户余额设为无限(unlimited)

1 participant