Skip to content

Feature/pool rolling window quota - #4332

Closed
bllackhu wants to merge 4 commits into
QuantumNous:mainfrom
bllackhu:feature/pool-rolling-window-quota
Closed

Feature/pool rolling window quota#4332
bllackhu wants to merge 4 commits into
QuantumNous:mainfrom
bllackhu:feature/pool-rolling-window-quota

Conversation

@bllackhu

@bllackhu bllackhu commented Apr 18, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

本 PR 引入 Pool(编码方案) 与 滚动窗口配额,并把管理端从行内表单改成 SideSheet,同时在服务端禁止同一池下重复绑定,减少配置错误。前后端均有单测覆盖;合并前请维护者关注 DB 迁移 与 历史重复绑定数据。

📝 变更描述 / Description

做了什么
在 relay 分发路径上增加 Pool 选择与滚动配额校验:请求进入后先按 token/用户等解析应落入的池,再在窗口内累计用量,超限则拒绝,从而把「按池限流」从纯前端配置变成请求链路中的硬约束。管理端新增 Pool 相关 Tab(池、绑定、通道、策略),并把创建/编辑收到 SideSheet,避免长表格里行内编辑易点错、难对照的问题。

为何能生效
滚动配额逻辑放在 middleware 层、与现有 distributor 衔接,保证命中路由的请求都会经过同一套计数与窗口判断;绑定去重在 CreatePoolBinding / UpdatePoolBinding 里对 (binding_type, binding_value, pool_id) 做唯一性约束,错误在写入前即返回,不依赖前端防呆。SideSheet 只改交互层,数据仍走原有 API,状态集中在 usePoolsData,列表刷新与筛选参数与之前同一数据源。

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

  • Closes # 无

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

Go(middleware + model,节选)

ok github.com/QuantumNous/new-api/middleware 0.140s
ok github.com/QuantumNous/new-api/model 0.169s
命令:go test ./middleware/... ./model/... -count=1 -short

前端(Pool hook 单测)

✓ src/hooks/pools/usePoolsData.test.jsx (4 tests) 114ms
Test Files 1 passed (1)
Tests 4 passed (4)
命令:npx vitest run src/hooks/pools/usePoolsData.test.jsx(Vitest 退出时若提示 close timed out,属环境/插件问题,用例已通过、exit code 0。)

Summary by CodeRabbit

  • New Features

    • Added full pool management UI and admin console for creating/managing pools, channels, policies, and bindings
    • Automatic pool selection and pool-aware channel routing during requests
    • Rolling-window quota enforcement and rolling-usage endpoints (admin + self)
  • Configuration

    • Pool, quota, and rolling-window toggles exposed via configuration/environment
  • Chores

    • Updated compose deployment settings and frontend test tooling/configuration
  • Tests

    • Added unit and integration tests for pool selection and quota behaviors

Refactor pool management tabs to use SideSheet create/edit flows, add clearer binding filtering and metadata display, and enforce duplicate binding guards per pool to reduce admin confusion and data mistakes.

Made-with: Cursor
@coderabbitai

coderabbitai Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feature/pool rolling window quota' directly and clearly describes the main feature being added: pool-based rolling window quota enforcement for request limiting.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

❤️ Share

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

…g-window-quota

Resolve conflicts in docker-compose.yml (keep named services, align Redis URL with requirepass), web/package.json (antd + axios 1.15.0), TokensColumnDefs (Token ID + upstream quota/group order with groupRatios). Refresh web/bun.lock for axios.

Made-with: Cursor

@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: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (2)
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)

61-70: ⚠️ Potential issue | 🟡 Minor

Seed admin.pool in the initial admin sidebar state.

The reset and parse-fallback defaults include pool: true, but the initial state does not. When no saved SidebarModulesAdmin option exists, the new Pool switch starts as undefined/off and can be saved that way.

✅ Proposed default-state fix
     admin: {
       enabled: true,
+      pool: true,
       channel: true,
       models: true,
       deployment: true,

Also applies to: 259-259

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

In `@web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx` around lines
61 - 70, The initial admin sidebar state in SettingsSidebarModulesAdmin.jsx is
missing the pool key so the new Pool switch defaults to undefined; update the
initial admin object (the admin property in the sidebar state) to include pool:
true alongside
enabled/channel/models/deployment/redemption/user/subscription/setting so it
matches the reset and parse-fallback defaults and ensures Pool starts enabled
when no saved SidebarModulesAdmin exists.
middleware/distributor.go (1)

39-53: ⚠️ Potential issue | 🟠 Major

Apply pool membership checks to token-specific channels too.

The new pool filtering covers affinity and random selection, but the ContextKeyTokenSpecificChannelId branch still accepts any enabled specific channel. A token pinned to a channel outside the selected pool bypasses pool isolation.

🛡️ Proposed pool check for specific-channel tokens
 		var channel *model.Channel
 		channelId, ok := common.GetContextKey(c, constant.ContextKeyTokenSpecificChannelId)
+		poolId := common.GetContextKeyInt(c, constant.ContextKeyPoolId)
 		modelRequest, shouldSelectChannel, err := getModelRequest(c)
 		if err != nil {
 			abortWithOpenAiMessage(c, http.StatusBadRequest, i18n.T(c, i18n.MsgDistributorInvalidRequest, map[string]any{"Error": err.Error()}))
 			return
@@
 			if channel.Status != common.ChannelStatusEnabled {
 				abortWithOpenAiMessage(c, http.StatusForbidden, i18n.T(c, i18n.MsgDistributorChannelDisabled))
 				return
 			}
+			if poolId > 0 {
+				inPool, poolErr := model.IsChannelInPool(poolId, channel.Id)
+				if poolErr != nil {
+					abortWithOpenAiMessage(c, http.StatusInternalServerError, "failed to check specific channel in pool")
+					return
+				}
+				if !inPool {
+					abortWithOpenAiMessage(c, http.StatusForbidden, "specific channel is not available in current pool")
+					return
+				}
+			}
 		} else {
@@
 				}
 				var selectGroup string
-				poolId := common.GetContextKeyInt(c, constant.ContextKeyPoolId)
 				usingGroup := common.GetContextKeyString(c, constant.ContextKeyUsingGroup)

Also applies to: 83-115, 143-150

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

In `@middleware/distributor.go` around lines 39 - 53, The token-specific-channel
branch (when using ContextKeyTokenSpecificChannelId) currently skips pool
membership checks allowing a token pinned to a channel outside the selected
pool; after retrieving channel via model.GetChannelById in that branch, verify
the channel belongs to the active pool (e.g., compare channel.PoolID or call the
existing pool-membership helper used in the pool-filter path) and abort with a
forbidden error (same i18n.MsgDistributorChannelDisabled or a new message) if it
does not; apply the same membership check logic to the other
token-specific-channel handling sites referenced (the other
ContextKeyTokenSpecificChannelId handling blocks around the other ranges) so
token-pinned channels cannot bypass pool isolation.
🟡 Minor comments (10)
docker-compose.yml-29-29 (1)

29-29: ⚠️ Potential issue | 🟡 Minor

Keep the MySQL example credentials in sync.

The commented MySQL DSN still uses 123456, but MYSQL_ROOT_PASSWORD is now change-me; uncommenting the documented MySQL path will fail authentication.

Proposed fix
-#      - SQL_DSN=root:123456@tcp(new-api-mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL
+#      - SQL_DSN=root:change-me@tcp(new-api-mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL

Also applies to: 80-80

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

In `@docker-compose.yml` at line 29, Update the commented MySQL DSN so it matches
the declared root password (MYSQL_ROOT_PASSWORD) instead of the hardcoded
123456; locate the commented line containing SQL_DSN and replace the password
portion with the actual secret or a reference to the env var (e.g., use
change-me or ${MYSQL_ROOT_PASSWORD}) so uncommenting SQL_DSN will authenticate
correctly.
web/src/components/layout/SiderBar.jsx-151-156 (1)

151-156: ⚠️ Potential issue | 🟡 Minor

i18n key should use a Chinese source string for consistency.

All other sidebar entries in this file use Chinese strings as i18n keys (e.g., t('渠道管理'), t('订阅管理'), t('模型管理')). Using t('Coding Plan') here breaks that convention — the English literal becomes the translation key, which means zh-CN renders "Coding Plan" by default and the key won't be picked up by the existing extraction workflow the same way.

Proposed fix
       {
-        text: t('Coding Plan'),
+        text: t('编码方案'),
         itemKey: 'pool',
         to: '/pool',
         className: isAdmin() ? '' : 'tableHiddle',
       },

Then add the 编码方案 key to web/src/i18n/locales/{lang}.json (e.g., "编码方案": "Coding Plan" in en.json).

As per coding guidelines: "Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys ... call t('中文key') in components."

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

In `@web/src/components/layout/SiderBar.jsx` around lines 151 - 156, Replace the
i18n key t('Coding Plan') in the sidebar entry object (the item with itemKey
'pool' in SiderBar.jsx) with a Chinese source string t('编码方案') to match the
project's i18n convention, then add the corresponding entries in each locale
JSON (e.g., in en.json add "编码方案": "Coding Plan", and mirror for other
languages) so the translation extraction and rendering remain consistent.
web/src/components/table/pools/modals/PoolBindingFormSideSheet.jsx-56-106 (1)

56-106: ⚠️ Potential issue | 🟡 Minor

UI strings bypass i18n.

Select options (token/user/group/default/subscription_plan), all Input placeholders (token_id, user_id, pool_id, priority, binding_value), and the literal Enabled label are hardcoded English. Per project i18n guidelines (i18next + t('中文key') with Chinese source keys in web/src/i18n/locales/{lang}.json), these should be wrapped in t(...) like the Create/Update/Cancel buttons already are.

As per coding guidelines: "Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components."

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

In `@web/src/components/table/pools/modals/PoolBindingFormSideSheet.jsx` around
lines 56 - 106, The UI strings in PoolBindingFormSideSheet are hardcoded instead
of using i18n; update Select option labels (the values shown:
'token','user','group','default','subscription_plan'), all Input placeholders
(token_id, user_id, pool_id, priority, binding_value) and the literal "Enabled"
label to use the i18next translator. Import and call useTranslation() in this
component, replace literal strings passed to Select.Option children,
Input.placeholder props and the Text content with t('...') using Chinese source
keys per project convention (mirror keys used elsewhere for
Create/Update/Cancel), and keep Select option values and formData.binding_type
values unchanged; ensure Switch label uses t('中文key') as well so all visible
text is localized.
middleware/pool_select.go-39-42 (1)

39-42: ⚠️ Potential issue | 🟡 Minor

ContextKeyPoolScopeKey is always user:<userId> regardless of how the pool was resolved.

The scope key is set to "user:" + strconv.Itoa(userId) unconditionally, even when ResolvePoolForContext matched via a token or group binding, and even when userId == 0 (producing the misleading "user:0"). The downstream rolling-quota logic in loadPoolQuotaScopePoliciesAndScopeKey overrides it to token:<id> when token policies exist, so functionally it may not matter today, but storing a possibly-stale scope key here is easy to misuse later.

Consider either (a) setting it based on the actual binding path that matched (token/user/group) or (b) deferring the scope-key write to the consumer that actually knows the scope. At minimum, guard against userId <= 0.

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

In `@middleware/pool_select.go` around lines 39 - 42, The middleware currently
always writes ContextKeyPoolScopeKey as "user:<userId>" which can be incorrect
(e.g. token/group bindings or userId==0); update the pool selection logic in
pool_select.go (where common.SetContextKey is called) to only set
ContextKeyPoolScopeKey when the resolved binding path indicates a user binding
and userId > 0 (use the resolution result from ResolvePoolForContext to detect
token/group/user), otherwise avoid writing the scope key here and let the
consumer (e.g. loadPoolQuotaScopePoliciesAndScopeKey) set the correct
"token:<id>" or "group:<id>" value; alternatively remove the unconditional
SetContextKey call for ContextKeyPoolScopeKey and add a guard so you never store
"user:0".
web/src/components/table/pools/modals/PoolChannelFormSideSheet.jsx-30-89 (1)

30-89: ⚠️ Potential issue | 🟡 Minor

Localize the new side-sheet labels with Chinese source keys.

This component introduces English t() keys, placeholders, and the raw Enabled label. Please switch user-facing strings to t('中文key') and add the corresponding flat locale entries. As per coding guidelines, “Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.”

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

In `@web/src/components/table/pools/modals/PoolChannelFormSideSheet.jsx` around
lines 30 - 89, Replace all hardcoded English strings in
PoolChannelFormSideSheet.jsx (titles, Tag labels, Button text, Input
placeholders 'pool_id','channel_id','weight','priority', and the raw 'Enabled'
Text) with translation calls t('中文Key...') and ensure the component imports and
uses useTranslation() to get t; add matching flat entries in both
web/src/i18n/locales/en.json and web/src/i18n/locales/zh.json (or the project
locale files) using Chinese source strings as the keys and the English/Chinese
values accordingly, keeping keys unique and descriptive (e.g., '创建通道' -> 'Create
Pool Channel', '启用' -> 'Enabled', etc.) so every user-facing string in render
(Title, Tag, Buttons, Input placeholders, and Switch label) is wrapped with
t('中文key').
model/pool_binding_resolution_test.go-15-20 (1)

15-20: ⚠️ Potential issue | 🟡 Minor

Clear the pool tables before each test as well as during cleanup.

Right now this helper only schedules post-test cleanup, so a test can start with stale rows if the shared test DB was not clean. Also check cleanup errors so isolation failures are visible.

Suggested test-isolation fix
 func truncatePoolBindingResolutionTables(t *testing.T) {
 	t.Helper()
+	require.NoError(t, DB.Exec("DELETE FROM pool_bindings").Error)
+	require.NoError(t, DB.Exec("DELETE FROM pools").Error)
 	t.Cleanup(func() {
-		DB.Exec("DELETE FROM pool_bindings")
-		DB.Exec("DELETE FROM pools")
+		require.NoError(t, DB.Exec("DELETE FROM pool_bindings").Error)
+		require.NoError(t, DB.Exec("DELETE FROM pools").Error)
 	})
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/pool_binding_resolution_test.go` around lines 15 - 20, The helper
truncatePoolBindingResolutionTables currently only schedules post-test cleanup
and ignores DB errors; modify it to run immediate DELETEs on "pool_bindings" and
"pools" at the start (so each test begins with a clean DB) and keep the existing
t.Cleanup deletes, and check the returned error from DB.Exec for both immediate
and cleanup calls (calling t.Fatalf or t.Fatalf-like failure on error) so
failures are surfaced; reference the truncatePoolBindingResolutionTables helper
and the DB.Exec calls for both "DELETE FROM pool_bindings" and "DELETE FROM
pools".
web/src/components/table/pools/modals/PoolPolicyFormSideSheet.jsx-31-103 (1)

31-103: ⚠️ Potential issue | 🟡 Minor

Localize the policy form labels and placeholders.

This new side sheet mixes English t() keys, English placeholders/options, and raw text like Enabled. Please use Chinese source keys consistently for all user-facing strings. As per coding guidelines, “Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.”

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

In `@web/src/components/table/pools/modals/PoolPolicyFormSideSheet.jsx` around
lines 31 - 103, Update PoolPolicyFormSideSheet to localize all user-facing
strings using the useTranslation() hook with Chinese source keys: replace raw
placeholders ("pool_id", "metric", "window_seconds", "limit_count"), Select
option labels ("token", "user"), the Text label "Enabled", and any
Title/Tag/Button visible text that currently uses English t('...') keys with
t('中文 key') calls (keep internal values like Select value='token' if needed but
translate the displayed label), referencing the component/JSX elements Tag,
Title, Button, Input placeholders, Select.Option, Text and Switch to find each
string; ensure each replacement uses the project's flat Chinese key naming
convention and that translations exist in web/src/i18n/locales/*.json.
web/src/components/table/pools/index.jsx-157-375 (1)

157-375: ⚠️ Potential issue | 🟡 Minor

Use the project i18n convention for the new Pool page copy.

The new tabs, placeholders, select labels, button text, and helper text are currently English literals or English t() keys. Please convert the user-facing copy to t('中文key') and add the flat locale entries. As per coding guidelines, “Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.”

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

In `@web/src/components/table/pools/index.jsx` around lines 157 - 375, The
component currently uses English literals and non-conforming t() keys across UI
pieces (Tabs/TabPane labels, Button texts like the Refresh/Create/Apply
Filter/Clear Filters, Input placeholders, Select options, helper Text near
Rolling Usage Query, and labels in usageQuery controls such as placeholders and
Select.Option values); update each user-facing string in this file to call
t('中文key') with appropriate Chinese-source keys and add corresponding flat
entries in the locale JSONs, and ensure useTranslation() is imported and used;
specifically review usages in functions/variables like Tabs (activeTab), TabPane
labels, Button onClick handlers
(openCreateBinding/openCreatePool/openCreateChannel/openCreatePolicy/queryUsage/querySelf),
Input placeholders and value setters (bindingValueFilter, bindingNameFilter,
channelPoolFilter, policyPoolFilter, usageQuery.*), Select.Option values for
scope_type/window and bindingTypeFilter options, and the helper Text under
Rolling Usage Query, replacing literals with t('中文key') and adding flat i18n
keys to web/src/i18n/locales/{lang}.json.
web/src/hooks/pools/usePoolsData.jsx-30-31 (1)

30-31: ⚠️ Potential issue | 🟡 Minor

Localize table labels, action text, and status tags.

The new table column titles, action buttons, and Enabled / Disabled tags are English literals generated from the hook. Please move these to t('中文key') so the Pool console follows the repository i18n contract. As per coding guidelines, “Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.”

Also applies to: 599-782

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

In `@web/src/hooks/pools/usePoolsData.jsx` around lines 30 - 31, The boolTag
helper returns hard-coded English labels; update usePoolsData.jsx to import and
call useTranslation() (const { t } = useTranslation()) and replace the string
literals in boolTag (and all literal column titles, action button text, and
status labels in the same file/range lines ~599-782) with t('中文key') calls using
flat Chinese keys per repo convention (e.g., t('已启用') / t('已禁用') for
Enabled/Disabled), and ensure each key matches the flat JSON in
web/src/i18n/locales/{lang}.json; keep the boolTag identifier but return t(...)
wrapped in the same <Tag> elements so UI behavior is unchanged.
model/pool.go-147-163 (1)

147-163: ⚠️ Potential issue | 🟡 Minor

Binding resolution silently swallows DB errors.

GetPoolById returns gorm.ErrRecordNotFound both when the pool is genuinely missing/disabled and is indistinguishable from a transient DB error at this call site. The loop only advances on err == nil && pool != nil, so any real error (connection drop, etc.) from GetPoolById is discarded and resolution falls through to the next binding / default pool — masking outages and potentially mis-routing requests.

Consider distinguishing gorm.ErrRecordNotFound from other errors (propagate the latter), or inline the lookup via a join so a single query returns only enabled pools.

pool, err := GetPoolById(binding.PoolId)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
    return nil, err
}
if pool != nil {
    return pool, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/pool.go` around lines 147 - 163, The function resolvePoolByBindingType
currently ignores non-NotFound DB errors from GetPoolById and can mask real
outages; modify the loop in resolvePoolByBindingType to check the error returned
by GetPoolById and propagate any error that is not gorm.ErrRecordNotFound (e.g.,
using errors.Is(err, gorm.ErrRecordNotFound)) instead of swallowing it,
returning that error immediately, and only continue when the error is a
NotFound; alternatively, replace the per-binding GetPoolById calls with a single
joined query that only selects enabled pools to avoid the multi-call failure
mode (refer to resolvePoolByBindingType, GetPoolById and
gorm.ErrRecordNotFound).
🧹 Nitpick comments (4)
relay/common/stream_status.go (1)

73-91: Lock ordering note for MergePriorSoftErrors.

The method acquires prior.mu then s.mu. Given the current single call site in stream_scanner.go (where prevStreamStatus is local and no goroutine will ever call s.MergePriorSoftErrors(prior) with arguments swapped), this is safe today. Worth a brief doc comment documenting that prior must not be the same instance as s and must not be concurrently targeted by a reverse merge, to avoid future AB/BA deadlocks if this helper is reused.

Minor: the copy on line 79 can be skipped when prior.ErrorCount == 0 / len(prior.Errors) == 0 to avoid a tiny allocation on the common path, but this is cosmetic.

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

In `@relay/common/stream_status.go` around lines 73 - 91, Add a doc comment above
MergePriorSoftErrors explaining the lock-ordering precondition: callers must
ensure prior != s and must not perform the reverse merge concurrently (i.e., no
concurrent call that would acquire s.mu then prior.mu) so callers avoid AB/BA
deadlocks; then optimize the fast path by early-returning when prior.ErrorCount
== 0 || len(prior.Errors) == 0 to skip the slice copy/allocation and mutex work
(keep the existing sequence of locking prior.mu then s.mu and the references to
prior.mu and s.mu unchanged).
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)

298-298: Use a Chinese source key for the new sidebar label.

t('Coding Plan') adds an English i18n key in a codepath where translation keys are expected to be Chinese source strings. Consider using a Chinese key and mapping it to “Coding Plan” in locale files if the English display is intentional.

🌐 Proposed i18n alignment
-        { key: 'pool', title: t('Coding Plan'), description: t('Pool与配额策略') },
+        { key: 'pool', title: t('编码方案'), description: t('编码方案与配额策略') },

As per coding guidelines, web/src/**/*.{ts,tsx,js,jsx} must use translation files with Chinese source strings as keys and call t('中文key') in components.

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

In `@web/src/components/settings/personal/cards/NotificationSettings.jsx` at line
298, The new sidebar entry uses an English i18n key; update the call in
NotificationSettings.jsx so the component uses a Chinese source key (e.g.,
replace t('Coding Plan') with t('编码计划') or another agreed Chinese key) for the
item with key 'pool' (the object { key: 'pool', title: t(...) , description: ...
}), and add the corresponding mapping in the locale files to map that Chinese
key to "Coding Plan" (and any other locales) so translations remain consistent
with the project's Chinese-key convention.
web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx (1)

259-259: Use a Chinese source key for this new module label.

t('Coding Plan') does not follow the project’s frontend i18n convention. Use a Chinese source key and put the English wording in locale JSON if needed.

🌐 Proposed i18n alignment
-        { key: 'pool', title: t('Coding Plan'), description: t('Pool与配额策略') },
+        { key: 'pool', title: t('编码方案'), description: t('编码方案与配额策略') },

As per coding guidelines, web/src/**/*.{ts,tsx,js,jsx} must use translation files with Chinese source strings as keys and call t('中文key') in components.

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

In `@web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx` at line 259,
Replace the English source string passed to the translator in the module entry
where key: 'pool' inside SettingsSidebarModulesAdmin.jsx (the object with title:
t('Coding Plan')) with a Chinese source key, e.g. title: t('编码计划'); then add the
corresponding mapping in the locale JSON files (e.g. en.json -> "编码计划": "Coding
Plan", zh.json -> "编码计划": "编码计划") so the component calls t('中文key') and
translations remain correct; keep the description as-is if it already uses
Chinese.
service/channel_select.go (1)

119-165: Minor: consider collapsing the pool/non-pool branches.

The if param.PoolID > 0 { GetRandomSatisfiedChannelByPool } else { GetRandomSatisfiedChannel } pattern is duplicated in both the "auto" group loop and the else-branch. Since GetRandomSatisfiedChannelByPool already short-circuits to GetRandomSatisfiedChannel when poolId <= 0 (per model/channel_cache.go:121-151), calling GetRandomSatisfiedChannelByPool(..., param.PoolID) unconditionally would remove the duplication without changing behavior.

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

In `@service/channel_select.go` around lines 119 - 165, Duplicate branching calls
to GetRandomSatisfiedChannelByPool and GetRandomSatisfiedChannel can be
collapsed: always call model.GetRandomSatisfiedChannelByPool(...) with
param.PoolID (it already falls back to GetRandomSatisfiedChannel when poolId <=
0), replacing the two if/else sites inside the auto-group selection (where
channel = model.GetRandomSatisfiedChannelByPool or
model.GetRandomSatisfiedChannel) and the else-branch where channel, err = ...;
remove the duplicated conditional on param.PoolID and call
GetRandomSatisfiedChannelByPool uniformly (keeping param.PoolID and other args
the same).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/pool.go`:
- Around line 200-205: The code sets defaults for req.Metric and req.ScopeType
but doesn't validate/normalize values, so add a call to the existing
normalizePoolPolicy(req) helper before persisting to ensure only supported
metrics/scopes (e.g., request_count and token/user) are accepted; update both
the SavePoolPolicy path where req.Metric/req.ScopeType are set and the
UpdatePoolPolicy handler (referencing normalizePoolPolicy, req.Metric,
req.ScopeType, model.PoolQuotaMetricRequestCount, model.PoolQuotaScopeUser) to
call normalizePoolPolicy(req) and return a validation error if normalization
fails instead of saving arbitrary strings.
- Around line 328-330: The current validation in the pool binding create/update
endpoints rejects empty binding_value even though ResolvePoolForContext and
tests use PoolBindingTypeDefault with binding_value == ""; update the validation
so binding_value is only required when req.BindingType !=
PoolBindingTypeDefault. Concretely, in the Create (the block checking
req.PoolId, req.BindingType, req.BindingValue) and the UpdatePoolBinding handler
(the analogous check at 349-351), change the condition to allow
strings.TrimSpace(req.BindingValue) == "" when req.BindingType ==
PoolBindingTypeDefault; keep the PoolId and BindingType checks as-is and reuse
the same helper/logic used by ResolvePoolForContext to detect
PoolBindingTypeDefault.
- Around line 466-480: The endpoint currently builds redisKey as
"pool:rq:events:%d:user:%d" which reads user-scoped usage even when requests are
authenticated by token; update the key construction in the handler (near
tokenId, ResolvePoolForContext, parseRollingWindow, redisKey) to choose the
scope based on tokenId (or the presence of a token-scoped policy): if tokenId >
0 use "pool:rq:events:%d:token:%d" with tokenId, otherwise keep the existing
"user:%d" key so the ZCount reads the same scope where PoolRollingQuota writes
usage.

In `@docker-compose.yml`:
- Line 12: The compose file currently embeds a shared DB password; replace the
hardcoded password with an environment variable and fail fast if it's not set:
in the postgres service env, remove the default value and use POSTGRES_PASSWORD
via the Compose variable expansion with a required check like
${POSTGRES_PASSWORD:?Postgres password not set} so Docker Compose/CLI will error
when missing, and ensure any other occurrences (lines referenced) are changed
similarly; also add a .env.example or document that POSTGRES_PASSWORD must be
provided via a local .env or deployment secret.
- Line 5: The compose file exposes http://localhost:6001 but the application
default ServerAddress in setting/system_setting/system_setting_old.go still
points to http://localhost:3000, causing broken external URLs; either update the
default ServerAddress constant/field in system_setting_old.go to
"http://localhost:6001" (look for the ServerAddress default or
NewDefault/SystemSetting initializer) or change docker-compose.yml port mapping
back to 3000:3000 to match the existing ServerAddress, and then update all
README files (README.md, README.zh_CN.md, README.fr.md, README.ja.md,
README.zh_TW.md) so the documented access URL matches the chosen port.

In `@middleware/pool_quota_rolling.go`:
- Around line 117-137: The current trim/count/check/reserve flow (calls to
trimPoolWindowEvents, countRollingWindowEvents, and reservePoolRequestEvent
using redisKey and requestId) is not atomic and can race under concurrency;
replace it with an atomic Redis operation (Lua script or WATCH/EXEC transaction)
that: performs the trimming, computes counts for all entries in validPolicies
windows, checks limits, and only if all policies allow, inserts the new event
and sets the appropriate TTL in one atomic operation; modify the calling code to
call a single new helper (e.g., reservePoolRequestEventAtomic) that returns
success/failure and error, and remove the separate trim/count/reserve sequence.

In `@model/ability.go`:
- Around line 146-190: The issue is that getPoolChannelQuery builds its ability
set by reusing getChannelQuery which calls getPriority on the unscoped
abilities, so priority selection ignores the pool join and can pick a priority
that has no pool members; fix by computing priorities using the same pool-scoped
join: add a new helper getPoolPriority(group, model, retry, poolId) (or modify
getPriority to accept an optional poolId) that applies the JOIN/Where used in
getPoolChannelQuery before selecting a priority, then use that pool-aware
priority when constructing the channel query in GetChannelByPool (i.e., replace
the call path that leads to getPriority via getChannelQuery). Also consolidate
the duplicate weighted-selection code in GetChannelByPool and GetChannel into a
shared helper (e.g., selectChannelFromAbilities([]Ability)) to avoid
duplication.

In `@model/channel_cache.go`:
- Around line 146-154: FilterChannelIDsByPool currently returns only IDs so
pool-specific PoolChannel.Weight and PoolChannel.Priority are lost; change
FilterChannelIDsByPool to return a slice of structs (channelID plus weight and
priority) instead of []string, update the call site here to accept that new
return type, and change getRandomChannelFromIDs to accept that slice and perform
selection using the pool-specific Weight/Priority (falling back to global
channel.GetPriority()/weight from channelsIDM when pool fields are zero/missing)
so memory-cache path mirrors GetChannelByPool's weighted/priority logic; update
any other callers of FilterChannelIDsByPool/getRandomChannelFromIDs accordingly.

In `@model/main.go`:
- Around line 283-286: Add a composite unique index on the PoolBinding model to
prevent duplicates by tagging the fields BindingType, BindingValue, and PoolId
with the same GORM unique index name and ordering; update the PoolBinding struct
so BindingType has `uniqueIndex:uk_pool_binding,priority:1`, BindingValue has
`uniqueIndex:uk_pool_binding,priority:2`, and PoolId has
`uniqueIndex:uk_pool_binding,priority:3` (matching the pattern used by
PoolChannel and PoolQuotaPolicy) so GORM enforces a DB-level unique constraint
across SQLite/MySQL/Postgres.

In `@model/pool.go`:
- Around line 299-304: DeletePool currently only deletes the Pool row and leaves
orphaned PoolChannel/PoolBinding/PoolQuotaPolicy rows; update DeletePool to
perform the removal in a DB transaction: begin a transaction via DB.Begin(),
then delete from PoolChannel, PoolBinding, and PoolQuotaPolicy where pool_id =
poolId (use the same poolId match used in ResolvePoolForContext/GetPoolById),
then delete the Pool row (e.g., DB.Where("id = ?", poolId).Delete(&Pool{})), and
commit; ensure you rollback on any error and return that error. Alternatively,
implement a soft-delete flag on Pool and make ResolvePoolForContext/GetPoolById
and GetPoolBindings/GetPoolChannels/GetPoolPolicies consistently ignore disabled
pools, but prefer the transactional hard-delete approach above for immediate
cleanup.
- Around line 63-72: The PoolBinding uniqueness check is racy because
CreatePoolBinding/UpdatePoolBinding use Count + Create/Updates without a DB
constraint; modify the PoolBinding struct to declare a UNIQUE composite index on
(BindingType, BindingValue, PoolId) (e.g. change the gorm tag for
idx_pool_binding to include "unique") so the database enforces uniqueness, add a
migration/backfill to deduplicate existing rows before applying the constraint,
and update CreatePoolBinding and UpdatePoolBinding to translate the DB
duplicate-key error into the existing user-facing "duplicate" error path instead
of relying solely on the pre-check.

In `@router/relay-router.go`:
- Around line 73-74: The /suno and Midjourney routes are skipping pool
middleware so ContextKeyPoolId isn't set and quota enforcement is bypassed;
update the route registration for the Suno handler (where TokenAuth() currently
flows straight to Distribute()) and the registerMjRouterGroup (Midjourney) to
insert middleware.PoolSelect() and middleware.PoolRollingQuota() before calling
Distribute(), mirroring how relayV1Router and the /gemini group apply
PoolSelect() and PoolRollingQuota(), so pool-aware channel selection and rolling
quota enforcement run and ContextKeyPoolId is populated.

In `@web/package.json`:
- Line 13: Remove the unused "antd" dependency from package.json: delete the
"antd": "^5.23.0" entry, then run the project package manager (npm install or
yarn install) to update node_modules and regenerate the lockfile
(package-lock.json or yarn.lock); confirm no imports of "antd" remain and run
the build/test to ensure nothing breaks (references: package.json entry "antd"
and existing UI usage of "@douyinfe/semi-ui").

In `@web/src/components/table/pools/modals/PoolFormSideSheet.jsx`:
- Around line 22-76: The form uses English and hardcoded strings and has UX
issues: update all visible strings to use t('中文key') (replace t('Update'),
t('Create'), t('Update Pool'), t('Create Pool'), t('Cancel') and the Input
placeholders and Select.Option labels with Chinese keys via useTranslation()),
fix the SideSheet placement to a fixed value (change placement={isEdit ? 'right'
: 'left'} to placement='right'), guard the select value so it never becomes
'undefined' (change String(formData.status) to String(formData.status ?? 1) or
ensure parent seeds status), and convert the loose Inputs into a Form with
labeled Form.Item fields (wrap name/description/status in a Form, add a required
validator for name) so validation and labels match other SideSheet forms; locate
these changes around isEdit, SideSheet, the Input components, Select and
formData.status references in PoolFormSideSheet.jsx.

---

Outside diff comments:
In `@middleware/distributor.go`:
- Around line 39-53: The token-specific-channel branch (when using
ContextKeyTokenSpecificChannelId) currently skips pool membership checks
allowing a token pinned to a channel outside the selected pool; after retrieving
channel via model.GetChannelById in that branch, verify the channel belongs to
the active pool (e.g., compare channel.PoolID or call the existing
pool-membership helper used in the pool-filter path) and abort with a forbidden
error (same i18n.MsgDistributorChannelDisabled or a new message) if it does not;
apply the same membership check logic to the other token-specific-channel
handling sites referenced (the other ContextKeyTokenSpecificChannelId handling
blocks around the other ranges) so token-pinned channels cannot bypass pool
isolation.

In `@web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx`:
- Around line 61-70: The initial admin sidebar state in
SettingsSidebarModulesAdmin.jsx is missing the pool key so the new Pool switch
defaults to undefined; update the initial admin object (the admin property in
the sidebar state) to include pool: true alongside
enabled/channel/models/deployment/redemption/user/subscription/setting so it
matches the reset and parse-fallback defaults and ensures Pool starts enabled
when no saved SidebarModulesAdmin exists.

---

Minor comments:
In `@docker-compose.yml`:
- Line 29: Update the commented MySQL DSN so it matches the declared root
password (MYSQL_ROOT_PASSWORD) instead of the hardcoded 123456; locate the
commented line containing SQL_DSN and replace the password portion with the
actual secret or a reference to the env var (e.g., use change-me or
${MYSQL_ROOT_PASSWORD}) so uncommenting SQL_DSN will authenticate correctly.

In `@middleware/pool_select.go`:
- Around line 39-42: The middleware currently always writes
ContextKeyPoolScopeKey as "user:<userId>" which can be incorrect (e.g.
token/group bindings or userId==0); update the pool selection logic in
pool_select.go (where common.SetContextKey is called) to only set
ContextKeyPoolScopeKey when the resolved binding path indicates a user binding
and userId > 0 (use the resolution result from ResolvePoolForContext to detect
token/group/user), otherwise avoid writing the scope key here and let the
consumer (e.g. loadPoolQuotaScopePoliciesAndScopeKey) set the correct
"token:<id>" or "group:<id>" value; alternatively remove the unconditional
SetContextKey call for ContextKeyPoolScopeKey and add a guard so you never store
"user:0".

In `@model/pool_binding_resolution_test.go`:
- Around line 15-20: The helper truncatePoolBindingResolutionTables currently
only schedules post-test cleanup and ignores DB errors; modify it to run
immediate DELETEs on "pool_bindings" and "pools" at the start (so each test
begins with a clean DB) and keep the existing t.Cleanup deletes, and check the
returned error from DB.Exec for both immediate and cleanup calls (calling
t.Fatalf or t.Fatalf-like failure on error) so failures are surfaced; reference
the truncatePoolBindingResolutionTables helper and the DB.Exec calls for both
"DELETE FROM pool_bindings" and "DELETE FROM pools".

In `@model/pool.go`:
- Around line 147-163: The function resolvePoolByBindingType currently ignores
non-NotFound DB errors from GetPoolById and can mask real outages; modify the
loop in resolvePoolByBindingType to check the error returned by GetPoolById and
propagate any error that is not gorm.ErrRecordNotFound (e.g., using
errors.Is(err, gorm.ErrRecordNotFound)) instead of swallowing it, returning that
error immediately, and only continue when the error is a NotFound;
alternatively, replace the per-binding GetPoolById calls with a single joined
query that only selects enabled pools to avoid the multi-call failure mode
(refer to resolvePoolByBindingType, GetPoolById and gorm.ErrRecordNotFound).

In `@web/src/components/layout/SiderBar.jsx`:
- Around line 151-156: Replace the i18n key t('Coding Plan') in the sidebar
entry object (the item with itemKey 'pool' in SiderBar.jsx) with a Chinese
source string t('编码方案') to match the project's i18n convention, then add the
corresponding entries in each locale JSON (e.g., in en.json add "编码方案": "Coding
Plan", and mirror for other languages) so the translation extraction and
rendering remain consistent.

In `@web/src/components/table/pools/index.jsx`:
- Around line 157-375: The component currently uses English literals and
non-conforming t() keys across UI pieces (Tabs/TabPane labels, Button texts like
the Refresh/Create/Apply Filter/Clear Filters, Input placeholders, Select
options, helper Text near Rolling Usage Query, and labels in usageQuery controls
such as placeholders and Select.Option values); update each user-facing string
in this file to call t('中文key') with appropriate Chinese-source keys and add
corresponding flat entries in the locale JSONs, and ensure useTranslation() is
imported and used; specifically review usages in functions/variables like Tabs
(activeTab), TabPane labels, Button onClick handlers
(openCreateBinding/openCreatePool/openCreateChannel/openCreatePolicy/queryUsage/querySelf),
Input placeholders and value setters (bindingValueFilter, bindingNameFilter,
channelPoolFilter, policyPoolFilter, usageQuery.*), Select.Option values for
scope_type/window and bindingTypeFilter options, and the helper Text under
Rolling Usage Query, replacing literals with t('中文key') and adding flat i18n
keys to web/src/i18n/locales/{lang}.json.

In `@web/src/components/table/pools/modals/PoolBindingFormSideSheet.jsx`:
- Around line 56-106: The UI strings in PoolBindingFormSideSheet are hardcoded
instead of using i18n; update Select option labels (the values shown:
'token','user','group','default','subscription_plan'), all Input placeholders
(token_id, user_id, pool_id, priority, binding_value) and the literal "Enabled"
label to use the i18next translator. Import and call useTranslation() in this
component, replace literal strings passed to Select.Option children,
Input.placeholder props and the Text content with t('...') using Chinese source
keys per project convention (mirror keys used elsewhere for
Create/Update/Cancel), and keep Select option values and formData.binding_type
values unchanged; ensure Switch label uses t('中文key') as well so all visible
text is localized.

In `@web/src/components/table/pools/modals/PoolChannelFormSideSheet.jsx`:
- Around line 30-89: Replace all hardcoded English strings in
PoolChannelFormSideSheet.jsx (titles, Tag labels, Button text, Input
placeholders 'pool_id','channel_id','weight','priority', and the raw 'Enabled'
Text) with translation calls t('中文Key...') and ensure the component imports and
uses useTranslation() to get t; add matching flat entries in both
web/src/i18n/locales/en.json and web/src/i18n/locales/zh.json (or the project
locale files) using Chinese source strings as the keys and the English/Chinese
values accordingly, keeping keys unique and descriptive (e.g., '创建通道' -> 'Create
Pool Channel', '启用' -> 'Enabled', etc.) so every user-facing string in render
(Title, Tag, Buttons, Input placeholders, and Switch label) is wrapped with
t('中文key').

In `@web/src/components/table/pools/modals/PoolPolicyFormSideSheet.jsx`:
- Around line 31-103: Update PoolPolicyFormSideSheet to localize all user-facing
strings using the useTranslation() hook with Chinese source keys: replace raw
placeholders ("pool_id", "metric", "window_seconds", "limit_count"), Select
option labels ("token", "user"), the Text label "Enabled", and any
Title/Tag/Button visible text that currently uses English t('...') keys with
t('中文 key') calls (keep internal values like Select value='token' if needed but
translate the displayed label), referencing the component/JSX elements Tag,
Title, Button, Input placeholders, Select.Option, Text and Switch to find each
string; ensure each replacement uses the project's flat Chinese key naming
convention and that translations exist in web/src/i18n/locales/*.json.

In `@web/src/hooks/pools/usePoolsData.jsx`:
- Around line 30-31: The boolTag helper returns hard-coded English labels;
update usePoolsData.jsx to import and call useTranslation() (const { t } =
useTranslation()) and replace the string literals in boolTag (and all literal
column titles, action button text, and status labels in the same file/range
lines ~599-782) with t('中文key') calls using flat Chinese keys per repo
convention (e.g., t('已启用') / t('已禁用') for Enabled/Disabled), and ensure each key
matches the flat JSON in web/src/i18n/locales/{lang}.json; keep the boolTag
identifier but return t(...) wrapped in the same <Tag> elements so UI behavior
is unchanged.

---

Nitpick comments:
In `@relay/common/stream_status.go`:
- Around line 73-91: Add a doc comment above MergePriorSoftErrors explaining the
lock-ordering precondition: callers must ensure prior != s and must not perform
the reverse merge concurrently (i.e., no concurrent call that would acquire s.mu
then prior.mu) so callers avoid AB/BA deadlocks; then optimize the fast path by
early-returning when prior.ErrorCount == 0 || len(prior.Errors) == 0 to skip the
slice copy/allocation and mutex work (keep the existing sequence of locking
prior.mu then s.mu and the references to prior.mu and s.mu unchanged).

In `@service/channel_select.go`:
- Around line 119-165: Duplicate branching calls to
GetRandomSatisfiedChannelByPool and GetRandomSatisfiedChannel can be collapsed:
always call model.GetRandomSatisfiedChannelByPool(...) with param.PoolID (it
already falls back to GetRandomSatisfiedChannel when poolId <= 0), replacing the
two if/else sites inside the auto-group selection (where channel =
model.GetRandomSatisfiedChannelByPool or model.GetRandomSatisfiedChannel) and
the else-branch where channel, err = ...; remove the duplicated conditional on
param.PoolID and call GetRandomSatisfiedChannelByPool uniformly (keeping
param.PoolID and other args the same).

In `@web/src/components/settings/personal/cards/NotificationSettings.jsx`:
- Line 298: The new sidebar entry uses an English i18n key; update the call in
NotificationSettings.jsx so the component uses a Chinese source key (e.g.,
replace t('Coding Plan') with t('编码计划') or another agreed Chinese key) for the
item with key 'pool' (the object { key: 'pool', title: t(...) , description: ...
}), and add the corresponding mapping in the locale files to map that Chinese
key to "Coding Plan" (and any other locales) so translations remain consistent
with the project's Chinese-key convention.

In `@web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx`:
- Line 259: Replace the English source string passed to the translator in the
module entry where key: 'pool' inside SettingsSidebarModulesAdmin.jsx (the
object with title: t('Coding Plan')) with a Chinese source key, e.g. title:
t('编码计划'); then add the corresponding mapping in the locale JSON files (e.g.
en.json -> "编码计划": "Coding Plan", zh.json -> "编码计划": "编码计划") so the component
calls t('中文key') and translations remain correct; keep the description as-is if
it already uses Chinese.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0ea91b18-c3b2-4036-ac4c-e4a607885ec0

📥 Commits

Reviewing files that changed from the base of the PR and between f995a86 and a9e8441.

📒 Files selected for processing (43)
  • common/constants.go
  • common/init.go
  • constant/context_key.go
  • controller/pool.go
  • controller/relay.go
  • docker-compose.yml
  • middleware/distributor.go
  • middleware/pool_quota_rolling.go
  • middleware/pool_quota_rolling_test.go
  • middleware/pool_select.go
  • model/ability.go
  • model/channel_cache.go
  • model/main.go
  • model/option.go
  • model/pool.go
  • model/pool_binding_resolution_test.go
  • relay/common/stream_status.go
  • relay/helper/stream_scanner.go
  • router/api-router.go
  • router/relay-router.go
  • service/channel_affinity_usage_cache_test.go
  • service/channel_select.go
  • web/package.json
  • web/src/App.jsx
  • web/src/components/layout/PageLayout.jsx
  • web/src/components/layout/SiderBar.jsx
  • web/src/components/settings/personal/cards/NotificationSettings.jsx
  • web/src/components/table/channels/ChannelsColumnDefs.jsx
  • web/src/components/table/pools/index.jsx
  • web/src/components/table/pools/modals/PoolBindingFormSideSheet.jsx
  • web/src/components/table/pools/modals/PoolChannelFormSideSheet.jsx
  • web/src/components/table/pools/modals/PoolFormSideSheet.jsx
  • web/src/components/table/pools/modals/PoolPolicyFormSideSheet.jsx
  • web/src/components/table/tokens/TokensColumnDefs.jsx
  • web/src/helpers/render.jsx
  • web/src/hooks/common/useSidebar.js
  • web/src/hooks/pools/usePoolsData.jsx
  • web/src/hooks/pools/usePoolsData.test.jsx
  • web/src/index.jsx
  • web/src/pages/Pool/index.jsx
  • web/src/pages/Setting/Operation/SettingsSidebarModulesAdmin.jsx
  • web/src/test/setupTests.js
  • web/vite.config.js

Comment thread controller/pool.go
Comment thread controller/pool.go
Comment thread controller/pool.go
Comment thread docker-compose.yml
Comment thread docker-compose.yml
Comment thread model/pool.go
Comment thread model/pool.go
Comment thread router/relay-router.go
Comment thread web/package.json
Comment thread web/src/components/table/pools/modals/PoolFormSideSheet.jsx

@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: 2

♻️ Duplicate comments (1)
docker-compose.yml (1)

28-30: ⚠️ Potential issue | 🟠 Major

Hardcoded Redis password also needs to move to env/secret (extends prior finding).

The prior review flagged the Postgres credential at lines 12/28/66 (now 68). The same concern applies to the new Redis password 123456qwerty on line 30 here and line 58 below — it is a committed shared secret and, per Betterleaks, line 68 is still flagged as a leaked generic key. Please consolidate both into .env-driven variables with required checks, e.g.:

Proposed fix
-      - SQL_DSN=postgresql://root:u0HjoW0z9uHzHu84EFrM7OcKZthscsNi@koooyooo-newapi-postgres:5432/new-api # ⚠️ IMPORTANT: Change the password in production!
-#      - SQL_DSN=root:123456qwerty@tcp(koooyooo-newapi-mysql:3306)/new-api  # Point to the mysql service, uncomment if using MySQL
-      - REDIS_CONN_STRING=redis://:123456qwerty@koooyooo-newapi-redis:6379 # ⚠️ IMPORTANT: Change the password in production! (must match koooyooo-newapi-redis requirepass)
+      - SQL_DSN=postgresql://root:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}@koooyooo-newapi-postgres:5432/new-api
+#      - SQL_DSN=root:${MYSQL_ROOT_PASSWORD:?set MYSQL_ROOT_PASSWORD in .env}@tcp(koooyooo-newapi-mysql:3306)/new-api
+      - REDIS_CONN_STRING=redis://:${REDIS_PASSWORD:?set REDIS_PASSWORD in .env}@koooyooo-newapi-redis:6379

And mirror ${REDIS_PASSWORD:?...} on line 58 and ${POSTGRES_PASSWORD:?...} on line 68. Add a .env.example documenting the required variables.

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

In `@docker-compose.yml` around lines 28 - 30, Replace the hardcoded Redis and
Postgres credentials with env-driven variables and runtime checks: stop
embedding the password in REDIS_CONN_STRING and SQL_DSN and instead construct
them from environment vars (e.g. use REDIS_PASSWORD and POSTGRES_PASSWORD) and
validate presence using the shell parameter expansion pattern (e.g.
${REDIS_PASSWORD:?} / ${POSTGRES_PASSWORD:?}) where REDIS_CONN_STRING and
SQL_DSN are defined; update the other occurrence of REDIS_CONN_STRING (the
duplicate at line 58) to use the same env var, and add a .env.example
documenting REDIS_PASSWORD, POSTGRES_PASSWORD and any host/port names so
deployments must supply secrets rather than committing them.
🧹 Nitpick comments (1)
docker-compose.yml (1)

18-18: Pinning :latest harms reproducibility.

harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest (and redis:latest on line 55) make deployments non-deterministic — two docker compose pull runs on different days can land on different app/middleware versions, which is particularly risky for this PR since the rolling-window quota logic depends on a specific Redis client/server contract. Prefer an immutable tag or digest for the app image, and pin redis to a supported major (e.g., redis:7-alpine).

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

In `@docker-compose.yml` at line 18, The compose file uses floating tags (image:
harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest and redis:latest)
which breaks reproducibility; update the service image entries to use an
immutable tag or digest for koooyooo-newapi (replace :latest with the exact
release tag or `@sha256`:... digest) and pin Redis to a supported major like
redis:7-alpine so the docker-compose image fields are deterministic and stable
for deployments.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docker-compose.yml`:
- Around line 17-19: The docker-compose change renames the
service/image/container to downstream-specific values (koooyooo-newapi, image
harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest, container_name
koooyooo-newapi-app) which is out of scope; revert these entries to the original
upstream generic names and image (e.g., new-api and the upstream image tag) or
move the rebranding into a separate PR/override file; specifically restore the
service key, the image value, and the container_name back to the upstream
identifiers or extract them into a separate compose override so upstream
docker-compose.yml remains generic.

In `@web/src/components/table/tokens/TokensColumnDefs.jsx`:
- Around line 485-494: The column title currently uses t('Token ID') in
TokensColumnDefs.jsx; replace this English key with the Chinese source key used
by your i18n files (for example t('令牌 ID') or the exact Chinese key used by
adjacent columns) in the column object where title is set, keeping the render
block with Typography.Text unchanged; also ensure the corresponding flat JSON
entries are added/updated in web/src/i18n/locales/{lang}.json so the key
resolves in all locales.

---

Duplicate comments:
In `@docker-compose.yml`:
- Around line 28-30: Replace the hardcoded Redis and Postgres credentials with
env-driven variables and runtime checks: stop embedding the password in
REDIS_CONN_STRING and SQL_DSN and instead construct them from environment vars
(e.g. use REDIS_PASSWORD and POSTGRES_PASSWORD) and validate presence using the
shell parameter expansion pattern (e.g. ${REDIS_PASSWORD:?} /
${POSTGRES_PASSWORD:?}) where REDIS_CONN_STRING and SQL_DSN are defined; update
the other occurrence of REDIS_CONN_STRING (the duplicate at line 58) to use the
same env var, and add a .env.example documenting REDIS_PASSWORD,
POSTGRES_PASSWORD and any host/port names so deployments must supply secrets
rather than committing them.

---

Nitpick comments:
In `@docker-compose.yml`:
- Line 18: The compose file uses floating tags (image:
harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest and redis:latest)
which breaks reproducibility; update the service image entries to use an
immutable tag or digest for koooyooo-newapi (replace :latest with the exact
release tag or `@sha256`:... digest) and pin Redis to a supported major like
redis:7-alpine so the docker-compose image fields are deterministic and stable
for deployments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 812a4d9d-60b6-4521-a57d-e92f772f2982

📥 Commits

Reviewing files that changed from the base of the PR and between a9e8441 and ddcdc18.

📒 Files selected for processing (11)
  • common/constants.go
  • common/init.go
  • constant/context_key.go
  • controller/relay.go
  • docker-compose.yml
  • model/option.go
  • router/api-router.go
  • web/package.json
  • web/src/components/layout/PageLayout.jsx
  • web/src/components/table/tokens/TokensColumnDefs.jsx
  • web/src/helpers/render.jsx
✅ Files skipped from review due to trivial changes (2)
  • web/package.json
  • web/src/components/layout/PageLayout.jsx
🚧 Files skipped from review as they are similar to previous changes (5)
  • constant/context_key.go
  • model/option.go
  • common/init.go
  • router/api-router.go
  • web/src/helpers/render.jsx

Comment thread docker-compose.yml
Comment on lines +17 to +19
koooyooo-newapi:
image: harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest
container_name: koooyooo-newapi-app

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

Rebranding appears out of scope for this PR.

This PR is titled "Feature/pool rolling window quota" and its stated objectives are middleware pool selection, sliding-window quota checks, uniqueness of pool bindings, and admin SideSheet UI changes. Renaming the service/image/container from new-api / calciumion/new-api:latest to koooyooo-newapi / harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest (and all derived hostnames, network name, and node name) is unrelated to the feature and points at a private registry fork.

If this is a downstream fork-specific change, please split it out of this PR so upstream docker-compose.yml stays generic; otherwise, please clarify in the PR description why upstream users should adopt the koooyooo-newapi naming and a private harbor image as defaults.

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

In `@docker-compose.yml` around lines 17 - 19, The docker-compose change renames
the service/image/container to downstream-specific values (koooyooo-newapi,
image harbor.circledigital.cn/koooyooo-api/koooyooo-newapi:latest,
container_name koooyooo-newapi-app) which is out of scope; revert these entries
to the original upstream generic names and image (e.g., new-api and the upstream
image tag) or move the rebranding into a separate PR/override file; specifically
restore the service key, the image value, and the container_name back to the
upstream identifiers or extract them into a separate compose override so
upstream docker-compose.yml remains generic.

Comment on lines +485 to +494
{
title: t('Token ID'),
dataIndex: 'id',
width: 120,
render: (text, record) => (
<Typography.Text copyable={{ content: String(record.id) }}>
{record.id}
</Typography.Text>
),
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use a Chinese source key for this column label.

Line 486 adds t('Token ID'), but this frontend uses Chinese source strings as i18n keys. Prefer a Chinese key consistent with the adjacent columns.

🌐 Proposed fix
     {
-      title: t('Token ID'),
+      title: t('令牌ID'),
       dataIndex: 'id',
       width: 120,

As per coding guidelines, “Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components.”

📝 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
{
title: t('Token ID'),
dataIndex: 'id',
width: 120,
render: (text, record) => (
<Typography.Text copyable={{ content: String(record.id) }}>
{record.id}
</Typography.Text>
),
},
{
title: t('令牌ID'),
dataIndex: 'id',
width: 120,
render: (text, record) => (
<Typography.Text copyable={{ content: String(record.id) }}>
{record.id}
</Typography.Text>
),
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/table/tokens/TokensColumnDefs.jsx` around lines 485 - 494,
The column title currently uses t('Token ID') in TokensColumnDefs.jsx; replace
this English key with the Chinese source key used by your i18n files (for
example t('令牌 ID') or the exact Chinese key used by adjacent columns) in the
column object where title is set, keeping the render block with Typography.Text
unchanged; also ensure the corresponding flat JSON entries are added/updated in
web/src/i18n/locales/{lang}.json so the key resolves in all locales.

@bllackhu bllackhu closed this Apr 18, 2026
@bllackhu

Copy link
Copy Markdown
Author

Opened by mistake; we deploy from our fork only.

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.

1 participant