Skip to content

feat(channel): add affinity-aware concurrency routing#6264

Open
Ericcc-Ma wants to merge 9 commits into
QuantumNous:mainfrom
Ericcc-Ma:feature/channel-max-concurrency
Open

feat(channel): add affinity-aware concurrency routing#6264
Ericcc-Ma wants to merge 9 commits into
QuantumNous:mainfrom
Ericcc-Ma:feature/channel-max-concurrency

Conversation

@Ericcc-Ma

@Ericcc-Ma Ericcc-Ma commented Jul 17, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

  • 请提供人工撰写的简洁摘要,避免直接粘贴未经整理的 AI 输出。

📝 变更描述 / Description

为渠道增加最大并发配置,并将并发限制接入普通分发与渠道亲和路由。候选渠道并发相同时按占用量优先选择;请求结束后释放占用,自动分组重试时保留正确的亲和状态。

同时补充默认前端配置入口、多语言文案、设计文档和相关回归测试。

🚀 变更类型 / 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 test ./... -count=1。除上游已有的 TestObserveChannelAffinityUsageCacheByRelayFormat_UnsupportedModeKeepsEmpty 缓存计数隔离失败外,其余包通过。

Summary by CodeRabbit

  • New Features
    • Added configurable per-channel concurrency limits, including a global enable/disable setting.
    • Added a “Max Concurrency” field to channel settings; 0 means unlimited.
    • Improved routing to prefer less-loaded channels and support affinity-aware fallback.
    • Added safeguards to release concurrency capacity after completed, failed, or retried requests.
  • Bug Fixes
    • Prevented concurrency slots from remaining occupied after relay errors or retries.
    • Improved multi-key channel selection and fallback behavior.
  • Documentation
    • Added design documentation covering affinity routing and concurrency management.
  • Tests
    • Added coverage for concurrency limits, channel selection, exclusions, and affinity updates.

Ericcc-Ma and others added 9 commits July 15, 2026 09:11
Enforce a per-channel concurrency cap via an in-memory semaphore
(service/channel_gate.go). SelectChannelWithLimits acquires the slot at
channel-selection time and excludes any channel at its cap, retrying
within the same priority tier without consuming the retry counter.
Multi-key channels also get per-key gating via selectHealthyKey.

The relay retry loop (controller/relay.go) and the distributor's first-
attempt selection (middleware/distributor.go) both release the acquired
slot on success and failure paths through releaseLimitGate, so slots are
never leaked. The channel cache / ability / RetryParam selection chain
gains an excludeIDs filter so a full channel is skipped on re-selection.

A global toggle (channel_limit_setting.enabled) gates the feature;
per-channel MaxConcurrency=0 means unlimited.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a "Max Concurrency" field to the channel edit drawer and wire it
through the channel form schema, defaults, and setting-JSON build/parse.
Add a global "Channel Limit Enabled" toggle to the routing-reliability
system-settings section. i18n entries added for en/zh/fr/ja/ru/vi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cover the concurrency gate (acquire/release, zero-max-unlimited,
in-flight never exceeds cap), the limit orchestrator (failed-concurrency
acquire skips and excludes; disabled limit allows everything; GateHandle
release is safe on nil/empty), the channel-cache excludeIDs filter, the
distributor limit-gate wiring, and GetChannelLimits merge behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Changes

Channel concurrency routing

Layer / File(s) Summary
Concurrency configuration and UI
constant/context_key.go, dto/channel_settings.go, setting/operation_setting/*, web/default/src/features/channels/..., web/default/src/features/system-settings/..., web/default/src/i18n/locales/*
Adds global enablement and per-channel max_concurrency settings, form serialization and validation, system settings controls, context keys, and translations.
Gate-backed channel and key selection
service/channel_gate.go, service/channel_limit*.go, model/ability.go, model/channel.go, model/channel_cache.go, service/channel_select.go, *_test.go
Adds non-blocking concurrency gates, least-loaded selection, channel and key exclusions, preselected multi-key indexes, and tests for acquisition, release, fallback, and ordering.
Affinity fallback and preselected keys
service/channel_affinity.go, middleware/distributor.go, related tests
Adds affinity update policies, concurrency-aware fallback exclusions, and context-based reuse of selected multi-key indexes.
Relay integration and gate release
controller/relay.go, controller/channel_upstream_update.go, controller/ratio_sync.go, relay/relay_task.go, relay/common/relay_info.go
Integrates limit-aware selection into relay attempts, releases handles across success, failure, retry, and body-read paths, and updates key-selection call sites.
Routing design specification
docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md
Documents affinity behavior, concurrency lifecycle, migration rules, error handling, observability, testing, and future extensions.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Request as Request
  participant Distributor as Distributor
  participant Selector as SelectChannelWithLimits
  participant Gate as ConcurrencyGate
  participant Relay as Relay
  participant Upstream as UpstreamChannel
  Request->>Distributor: begin channel selection
  Distributor->>Selector: select with affinity exclusions
  Selector->>Gate: acquire channel/key slot
  Gate-->>Selector: return acquisition result
  Selector-->>Distributor: return channel, key index, and handle
  Distributor->>Relay: configure selected channel
  Relay->>Upstream: execute request
  Upstream-->>Relay: return response or error
  Relay->>Gate: release slot
Loading

Possibly related PRs

Suggested reviewers: seefs001

Poem

I’m a rabbit guarding gates in the hay,
Slots open and close through the day.
Affinity hops near, fallback hops wide,
Keys keep their place by the channel’s side.
Retries release what they borrow with care—
Clean little paths through the routing air.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.39% 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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: affinity-aware routing with per-channel concurrency limits.
✨ 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.

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

Caution

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

⚠️ Outside diff range comments (2)
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx (1)

992-997: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include max_concurrency in the configured state.

The max_concurrency field is not included in the routingStrategyConfigured evaluation. If a user customizes only the max_concurrency setting and leaves priority and weight at defaults, the "Routing Strategy" section will incorrectly appear as unconfigured in the navigation menu.

Please watch the max_concurrency field and include it in the boolean check.

🐛 Proposed fix

First, watch the new field around Line 731:

   const currentPriority = form.watch('priority')
   const currentWeight = form.watch('weight')
+  const currentMaxConcurrency = form.watch('max_concurrency')
   const currentTestModel = form.watch('test_model')

Then, include it in the condition here:

   const routingStrategyConfigured = Boolean(
     currentPriority ||
     currentWeight ||
+    currentMaxConcurrency ||
     currentTestModel?.trim() ||
     (currentAutoBan ?? 1) !== 1
   )
🤖 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/channels/components/drawers/channel-mutate-drawer.tsx`
around lines 992 - 997, Update the routing strategy state in the channel mutate
drawer to track the max_concurrency field alongside the existing priority,
weight, test model, and auto-ban values, then include that watched value in
routingStrategyConfigured so customizing only max_concurrency marks the section
as configured.
relay/relay_task.go (1)

92-100: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Transfer the concurrency reservation when switching channels.

When the origin task uses another channel, this overwrites the selected channel and key but leaves the existing gate handle attached to the distributor-selected channel. The origin channel therefore bypasses its limit while an unrelated slot remains occupied. Release the current handle and acquire limits for ch before updating the context.

🤖 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 `@relay/relay_task.go` around lines 92 - 100, When switching channels in the
origin-task handling branch, release the existing concurrency gate handle and
acquire a new reservation for ch before updating the channel context keys.
Ensure the new handle is attached to the origin channel and preserve the
existing GetNextEnabledKey error handling.
🧹 Nitpick comments (5)
docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md (1)

183-193: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Document per-key gates and their capacity composition.

The specification documents only channel:<channel_id>, while service/channel_limit_select.go also uses channel:<id>:key:<idx> gates and GateHandle can release both. Clarify whether a request acquires both channel-level and key-level slots, and how those limits compose, so the design, metrics, and tests do not assume a single counter.

🤖 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 `@docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md`
around lines 183 - 193, Update the concurrency-routing design section describing
per-process semaphores to document both channel-level keys and the channel-key
gates used by service/channel_limit_select.go. Specify whether requests acquire
both GateHandle-associated slots, how their capacities compose, and how
acquisition, release, read-only concurrency, metrics, and tests account for the
two independent limits; preserve the existing single-process and atomic
TryAcquireConcurrency semantics.
web/default/src/features/models/components/drawers/model-mutate-drawer.tsx (1)

216-216: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove unused default setting.

channel_limit_setting.enabled was added to defaultModelSettings here but is not utilized anywhere else in this component (which primarily processes pricing and ratio configs). Unless it is strictly required by the ModelSettings type definition, consider removing it to reduce clutter.

♻️ Proposed refactor
-      'channel_limit_setting.enabled': true,
🤖 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/models/components/drawers/model-mutate-drawer.tsx`
at line 216, Remove the unused channel_limit_setting.enabled entry from
defaultModelSettings in the model mutation drawer, unless ModelSettings
explicitly requires it; preserve the remaining pricing and ratio defaults
unchanged.
service/channel_limit_select_test.go (1)

80-83: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use testify/assert for non-fatal value checks.

As per coding guidelines, new or substantially rewritten Go backend tests must use testify/assert for non-fatal value checks. Consider replacing require.Len and require.Equal with assert.Len and assert.Equal for the final test assertions. This applies to similar assertions on lines 95-96, 153-154, and 171-172.

♻️ Proposed refactor
 	leastLoaded := leastLoadedChannels(channels)
-	require.Len(t, leastLoaded, 1)
-	require.Equal(t, 9412, leastLoaded[0].Id)
+	assert.Len(t, leastLoaded, 1)
+	assert.Equal(t, 9412, leastLoaded[0].Id)
 }
🤖 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 `@service/channel_limit_select_test.go` around lines 80 - 83, Update the final
assertions in the channel-limit selection tests, including the checks around
leastLoadedChannels and the similar assertions at the referenced test sections,
to use testify/assert.Len and assert.Equal instead of require.Len and
require.Equal. Preserve the existing assertion values and test behavior.

Source: Coding guidelines

controller/relay.go (2)

613-618: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant releaseLimitGate call.

Similar to the standard relay loop, the releaseLimitGate(c) call on line 616 is redundant because the gate was already released unconditionally on line 605.

♻️ Proposed refactor
 		if !shouldRetryTaskRelay(c, channel.Id, taskErr, common.RetryTimes-retryParam.GetRetry()) {
-			// not retrying: release the slot held by this iteration's handle so
-			// we don't leak the semaphore across the rest of the process lifetime.
-			releaseLimitGate(c)
 			break
 		}
🤖 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 `@controller/relay.go` around lines 613 - 618, Remove the redundant
releaseLimitGate(c) call from the non-retry branch of the relay loop around
shouldRetryTaskRelay; the gate is already released unconditionally earlier in
this iteration. Preserve the existing retry decision and loop-break behavior.

229-245: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove redundant releaseLimitGate call.

The releaseLimitGate(c) call on line 243 is redundant because it is already called unconditionally on line 236 before processChannelError. Since releaseLimitGate clears the context key, the second invocation acts as a no-op.

♻️ Proposed refactor
 		if !shouldRetry(c, newAPIError, common.RetryTimes-retryParam.GetRetry()) {
-			// not retrying: release the slot held by this iteration's handle so
-			// we don't leak the semaphore across the rest of the process lifetime.
-			releaseLimitGate(c)
 			break
 		}
🤖 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 `@controller/relay.go` around lines 229 - 245, Remove the conditional
releaseLimitGate(c) call inside the !shouldRetry branch of the retry flow. Keep
the unconditional releaseLimitGate(c) call before processChannelError, and
preserve the existing break behavior when retries are exhausted.
🤖 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.

Inline comments:
In
`@docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md`:
- Around line 181-193: 明确并落实并发限制变更策略:在设计文档中说明渠道限制变更是否要求进程重启,或在
service/channel_gate.go 的 gate 管理逻辑中支持安全动态调整(包括 10→2 和 2→10)。若支持动态调整,避免
LoadOrStore 让旧容量永久生效,并安全淘汰旧 gate;同时为两个变更方向补充回归测试。

In `@middleware/distributor.go`:
- Around line 110-132: Update the affinity auto-group loop around
GetUserAutoGroup to capture the selected group’s index along with
preferredGroup. When setting ContextKeyAutoGroup after acquiring the preferred
channel, also store the corresponding ContextKeyAutoGroupIndex, matching the
normal selection path so retries resume from the affinity-selected group.

In `@model/ability.go`:
- Around line 162-169: Replace the per-ability DB.First call in the
channel-loading loop with a single query using all eligible ability.ChannelId
values and an IN clause. Index the fetched channels by ID, then rebuild channels
in the original abilities order, preserving error handling and the existing
result type.

In `@model/channel_cache.go`:
- Around line 213-241: Update GetSatisfiedChannels to sort the assembled
channels by descending priority before returning them, using the Channel
priority field and adding the sort import if needed. Preserve the existing
filtering, exclusion, cache lookup, and missing-channel error behavior.

In `@model/channel.go`:
- Around line 233-236: Update the polling loop that selects channel keys to
require !excludeKeyIdx[idx] in addition to the existing status check, ensuring
excluded keys are never returned even when capacity is saturated. Keep the
enabledIdx filtering and other polling behavior unchanged.

In `@service/channel_gate.go`:
- Around line 24-29: Update TryAcquireConcurrency so an existing concurrency
gate reflects the current max when the configured limit changes, rather than
permanently reusing the first channel capacity. Store a gate object with safely
updateable limit/state, or replace the existing gate only when it can be drained
without violating active acquisitions; preserve unlimited behavior for max <= 0
and ensure concurrent callers cannot bypass the configured limit.

In `@service/channel_limit_select.go`:
- Around line 81-94: Update selectHealthyKey to retain the successful
TryAcquireConcurrency result instead of releasing it before returning, and
return the selected index together with that retained acquisition. Ensure no
release occurs when MaxConcurrency <= 0, and attach the retained acquisition
directly to the caller’s GateHandle so reservation remains atomic.

In `@web/default/src/features/channels/constants.ts`:
- Line 362: Add the exact literal string used by
FIELD_DESCRIPTIONS.MAX_CONCURRENCY to the static key registry in static-keys.ts,
preserving the existing key format and placement conventions so dynamic lookup
extraction recognizes it.

In `@web/default/src/i18n/locales/en.json`:
- Line 2028: Restore the protected Unicode-escaped key
footer.new\u0061pi.projectAttributionSuffix in both
web/default/src/i18n/locales/en.json at lines 2028-2028 and
web/default/src/i18n/locales/fr.json at lines 2028-2028; leave the translation
values unchanged.

In `@web/default/src/i18n/locales/fr.json`:
- Around line 5182-5186: Translate the English values for “Channel Limit
Enabled,” “Channel Limits,” “Max Concurrency,” and “Max concurrent requests to
this channel (0 = unlimited)” in the French locale, while preserving the
existing keys and JSON structure.

In `@web/default/src/i18n/locales/ja.json`:
- Around line 5182-5186: Translate the user-facing values for “Channel Limit
Enabled,” “Channel Limits,” “Max Concurrency,” and “Max concurrent requests to
this channel (0 = unlimited)” in the Japanese locale, while preserving the
existing translation for “Master switch for per-channel concurrency limits.”

In `@web/default/src/i18n/locales/ru.json`:
- Around line 5182-5186: Translate the four newly added concurrency-limit
entries in the Russian locale—“Channel Limit Enabled”, “Channel Limits”, “Max
Concurrency”, and “Max concurrent requests to this channel (0 =
unlimited)”—while preserving the existing Russian translation for the
master-switch description.

In `@web/default/src/i18n/locales/vi.json`:
- Around line 5182-5183: Translate the Vietnamese locale values for the “Channel
Limit Enabled” and “Channel Limits” keys in the locale data, replacing the
English text while preserving the existing keys and JSON structure.

---

Outside diff comments:
In `@relay/relay_task.go`:
- Around line 92-100: When switching channels in the origin-task handling
branch, release the existing concurrency gate handle and acquire a new
reservation for ch before updating the channel context keys. Ensure the new
handle is attached to the origin channel and preserve the existing
GetNextEnabledKey error handling.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 992-997: Update the routing strategy state in the channel mutate
drawer to track the max_concurrency field alongside the existing priority,
weight, test model, and auto-ban values, then include that watched value in
routingStrategyConfigured so customizing only max_concurrency marks the section
as configured.

---

Nitpick comments:
In `@controller/relay.go`:
- Around line 613-618: Remove the redundant releaseLimitGate(c) call from the
non-retry branch of the relay loop around shouldRetryTaskRelay; the gate is
already released unconditionally earlier in this iteration. Preserve the
existing retry decision and loop-break behavior.
- Around line 229-245: Remove the conditional releaseLimitGate(c) call inside
the !shouldRetry branch of the retry flow. Keep the unconditional
releaseLimitGate(c) call before processChannelError, and preserve the existing
break behavior when retries are exhausted.

In
`@docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md`:
- Around line 183-193: Update the concurrency-routing design section describing
per-process semaphores to document both channel-level keys and the channel-key
gates used by service/channel_limit_select.go. Specify whether requests acquire
both GateHandle-associated slots, how their capacities compose, and how
acquisition, release, read-only concurrency, metrics, and tests account for the
two independent limits; preserve the existing single-process and atomic
TryAcquireConcurrency semantics.

In `@service/channel_limit_select_test.go`:
- Around line 80-83: Update the final assertions in the channel-limit selection
tests, including the checks around leastLoadedChannels and the similar
assertions at the referenced test sections, to use testify/assert.Len and
assert.Equal instead of require.Len and require.Equal. Preserve the existing
assertion values and test behavior.

In `@web/default/src/features/models/components/drawers/model-mutate-drawer.tsx`:
- Line 216: Remove the unused channel_limit_setting.enabled entry from
defaultModelSettings in the model mutation drawer, unless ModelSettings
explicitly requires it; preserve the remaining pricing and ratio defaults
unchanged.
🪄 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: 95fb1fcb-f896-485f-bc66-5d45929cab7f

📥 Commits

Reviewing files that changed from the base of the PR and between a63364d and 684ab61.

📒 Files selected for processing (39)
  • constant/context_key.go
  • controller/channel_upstream_update.go
  • controller/ratio_sync.go
  • controller/relay.go
  • docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md
  • dto/channel_settings.go
  • middleware/distributor.go
  • middleware/distributor_test.go
  • model/ability.go
  • model/channel.go
  • model/channel_cache.go
  • model/channel_cache_exclude_test.go
  • model/channel_candidates_test.go
  • relay/common/relay_info.go
  • relay/relay_task.go
  • service/channel_affinity.go
  • service/channel_affinity_template_test.go
  • service/channel_gate.go
  • service/channel_gate_test.go
  • service/channel_limit.go
  • service/channel_limit_select.go
  • service/channel_limit_select_test.go
  • service/channel_limit_test.go
  • service/channel_select.go
  • setting/operation_setting/channel_limit_setting.go
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/constants.ts
  • web/default/src/features/channels/lib/channel-form.ts
  • web/default/src/features/models/components/drawers/model-mutate-drawer.tsx
  • web/default/src/features/system-settings/models/index.tsx
  • web/default/src/features/system-settings/models/routing-reliability-section.tsx
  • web/default/src/features/system-settings/models/section-registry.tsx
  • web/default/src/features/system-settings/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

Comment on lines +181 to +193
## 8. 并发状态与多实例语义

继续使用进程内信号量实现渠道并发限制:

```text
channel:<channel_id>
```

Affinity 和非 Affinity 请求共享同一个 key。

该限制为单进程限制。多实例部署时,每个实例都拥有独立上限。例如渠道上限为 10,部署 3 个实例时,理论最大总并发为 30。这是现有并发 gate 的既有语义,本阶段不改为 Redis 全局计数。

需要提供安全的只读当前并发方法,供最少连接选择使用。读取值只用于排序,最终正确性仍由原子 `TryAcquireConcurrency` 保证。

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.

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Define the live-update policy for concurrency limits.

service/channel_gate.go creates gates with LoadOrStore(..., make(chan struct{}, max)), so the first configured limit becomes sticky for that process-local gate. Changing a channel from 10→2 or 2→10 will not resize the existing gate. Specify and implement a safe policy—such as versioned gate keys and retirement of old gates—or explicitly require process reinitialization, with regression tests for both directions.

🤖 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 `@docs/superpowers/specs/2026-07-15-glm-affinity-concurrency-routing-design.md`
around lines 181 - 193, 明确并落实并发限制变更策略:在设计文档中说明渠道限制变更是否要求进程重启,或在
service/channel_gate.go 的 gate 管理逻辑中支持安全动态调整(包括 10→2 和 2→10)。若支持动态调整,避免
LoadOrStore 让旧容量永久生效,并安全淘汰旧 gate;同时为两个变更方向补充回归测试。

Comment thread middleware/distributor.go
Comment on lines +110 to +132
preferredGroup := ""
if usingGroup == "auto" {
userGroup := common.GetContextKeyString(c, constant.ContextKeyUserGroup)
autoGroups := service.GetUserAutoGroup(userGroup)
for _, g := range autoGroups {
if model.IsChannelEnabledForGroupModel(g, modelRequest.Model, preferred.Id) {
selectGroup = g
common.SetContextKey(c, constant.ContextKeyAutoGroup, g)
channel = preferred
affinityUsable = true
service.MarkChannelAffinityUsed(c, g, preferred.Id)
preferredGroup = g
break
}
}
} else if model.IsChannelEnabledForGroupModel(usingGroup, modelRequest.Model, preferred.Id) {
channel = preferred
selectGroup = usingGroup
affinityUsable = true
service.MarkChannelAffinityUsed(c, usingGroup, preferred.Id)
preferredGroup = usingGroup
}
if preferredGroup != "" {
affinityCandidateValid = true
handle, acquired := service.AcquireChannelWithLimits(c, preferred)
if acquired {
channel = preferred
selectGroup = preferredGroup
common.SetContextKey(c, constant.ContextKeyChannelLimitGate, handle)
if usingGroup == "auto" {
common.SetContextKey(c, constant.ContextKeyAutoGroup, preferredGroup)
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the affinity-selected auto-group index.

This path stores ContextKeyAutoGroup but not ContextKeyAutoGroupIndex. A later SelectChannelWithLimits retry therefore restarts from the first auto group rather than the affinity channel’s group. Capture the loop index and store both values, matching the normal selection path.

🤖 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 `@middleware/distributor.go` around lines 110 - 132, Update the affinity
auto-group loop around GetUserAutoGroup to capture the selected group’s index
along with preferredGroup. When setting ContextKeyAutoGroup after acquiring the
preferred channel, also store the corresponding ContextKeyAutoGroupIndex,
matching the normal selection path so retries resume from the affinity-selected
group.

Comment thread model/ability.go
Comment on lines +162 to +169
channels := make([]*Channel, 0, len(abilities))
for _, ability := range abilities {
var channel Channel
if err := DB.First(&channel, "id = ?", ability.ChannelId).Error; err != nil {
return nil, err
}
channels = append(channels, &channel)
}

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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Batch-load candidate channels to avoid an N+1 query.

This performs one DB.First call for every eligible ability on the routing hot path. Fetch all channel IDs with one IN query, index them by ID, then rebuild the result in ability order.

🤖 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 `@model/ability.go` around lines 162 - 169, Replace the per-ability DB.First
call in the channel-loading loop with a single query using all eligible
ability.ChannelId values and an IN clause. Index the fetched channels by ID,
then rebuild channels in the original abilities order, preserving error handling
and the existing result type.

Comment thread model/channel_cache.go
Comment on lines +213 to +241
// GetSatisfiedChannels returns every enabled channel that can serve the
// request, ordered by descending priority. The returned slice is independent
// from the cached channel ID slices and is safe for the caller to reorder.
func GetSatisfiedChannels(group string, modelName string, requestPath string, excludeIDs []int) ([]*Channel, error) {
if !common.MemoryCacheEnabled {
return getSatisfiedChannelsFromDB(group, modelName, requestPath, excludeIDs)
}

channelSyncLock.RLock()
defer channelSyncLock.RUnlock()

channelIDs := filterChannelsByRequestPathAndModel(group2model2channels[group][modelName], requestPath, modelName)
if len(channelIDs) == 0 {
normalizedModel := ratio_setting.FormatMatchingModelName(modelName)
channelIDs = filterChannelsByRequestPathAndModel(group2model2channels[group][normalizedModel], requestPath, modelName)
}
channelIDs = excludeChannelIDs(channelIDs, excludeIDs)

channels := make([]*Channel, 0, len(channelIDs))
for _, channelID := range channelIDs {
channel, ok := channelsIDM[channelID]
if !ok {
return nil, fmt.Errorf("channel #%d does not exist", channelID)
}
channels = append(channels, channel)
}
return channels, nil
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ensure the returned channels are ordered by descending priority.

The function documentation explicitly states that the returned channels will be "ordered by descending priority." However, the current implementation simply returns them in the order they appear in group2model2channels, which is not guaranteed to be strictly sorted by priority. If downstream logic (like leastLoadedChannels or affinity fallback) relies on this exact ordering, it could lead to incorrect channel selection.

♻️ Proposed fix
 	for _, channelID := range channelIDs {
 		channel, ok := channelsIDM[channelID]
 		if !ok {
 			return nil, fmt.Errorf("channel #%d does not exist", channelID)
 		}
 		channels = append(channels, channel)
 	}
+
+	sort.Slice(channels, func(i, j int) bool {
+		return channels[i].GetPriority() > channels[j].GetPriority()
+	})
+
 	return channels, nil
 }

Note: You will also need to import "sort" if it's not already imported in this block.

📝 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
// GetSatisfiedChannels returns every enabled channel that can serve the
// request, ordered by descending priority. The returned slice is independent
// from the cached channel ID slices and is safe for the caller to reorder.
func GetSatisfiedChannels(group string, modelName string, requestPath string, excludeIDs []int) ([]*Channel, error) {
if !common.MemoryCacheEnabled {
return getSatisfiedChannelsFromDB(group, modelName, requestPath, excludeIDs)
}
channelSyncLock.RLock()
defer channelSyncLock.RUnlock()
channelIDs := filterChannelsByRequestPathAndModel(group2model2channels[group][modelName], requestPath, modelName)
if len(channelIDs) == 0 {
normalizedModel := ratio_setting.FormatMatchingModelName(modelName)
channelIDs = filterChannelsByRequestPathAndModel(group2model2channels[group][normalizedModel], requestPath, modelName)
}
channelIDs = excludeChannelIDs(channelIDs, excludeIDs)
channels := make([]*Channel, 0, len(channelIDs))
for _, channelID := range channelIDs {
channel, ok := channelsIDM[channelID]
if !ok {
return nil, fmt.Errorf("channel #%d does not exist", channelID)
}
channels = append(channels, channel)
}
return channels, nil
}
// GetSatisfiedChannels returns every enabled channel that can serve the
// request, ordered by descending priority. The returned slice is independent
// from the cached channel ID slices and is safe for the caller to reorder.
func GetSatisfiedChannels(group string, modelName string, requestPath string, excludeIDs []int) ([]*Channel, error) {
if !common.MemoryCacheEnabled {
return getSatisfiedChannelsFromDB(group, modelName, requestPath, excludeIDs)
}
channelSyncLock.RLock()
defer channelSyncLock.RUnlock()
channelIDs := filterChannelsByRequestPathAndModel(group2model2channels[group][modelName], requestPath, modelName)
if len(channelIDs) == 0 {
normalizedModel := ratio_setting.FormatMatchingModelName(modelName)
channelIDs = filterChannelsByRequestPathAndModel(group2model2channels[group][normalizedModel], requestPath, modelName)
}
channelIDs = excludeChannelIDs(channelIDs, excludeIDs)
channels := make([]*Channel, 0, len(channelIDs))
for _, channelID := range channelIDs {
channel, ok := channelsIDM[channelID]
if !ok {
return nil, fmt.Errorf("channel #%d does not exist", channelID)
}
channels = append(channels, channel)
}
sort.Slice(channels, func(i, j int) bool {
return channels[i].GetPriority() > channels[j].GetPriority()
})
return channels, nil
}
🤖 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 `@model/channel_cache.go` around lines 213 - 241, Update GetSatisfiedChannels
to sort the assembled channels by descending priority before returning them,
using the Channel priority field and adding the sort import if needed. Preserve
the existing filtering, exclusion, cache lookup, and missing-channel error
behavior.

Comment thread model/channel.go
Comment on lines +233 to +236
// Collect indexes of enabled, non-excluded keys
enabledIdx := make([]int, 0, len(keys))
for i := range keys {
if getStatus(i) == common.ChannelStatusEnabled {
if getStatus(i) == common.ChannelStatusEnabled && !excludeKeyIdx[i] {

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Honor exclusions in polling mode too.

The new filter only affects enabledIdx; the polling loop at Lines 274–279 checks status alone and can still return an excluded, capacity-saturated key. Add !excludeKeyIdx[idx] to that loop’s condition.

🤖 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 `@model/channel.go` around lines 233 - 236, Update the polling loop that
selects channel keys to require !excludeKeyIdx[idx] in addition to the existing
status check, ensuring excluded keys are never returned even when capacity is
saturated. Keep the enabledIdx filtering and other polling behavior unchanged.

"footer.columns.related.title": "Related Projects",
"footer.defaultCopyright": "All rights reserved.",
"footer.new\u0061pi.projectAttributionSuffix": "All rights reserved. Designed and developed by the project contributors.",
"footer.newapi.projectAttributionSuffix": "All rights reserved. Designed and developed by the project contributors.",

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Restore protected project identity reference.

As per coding guidelines, "Protected references to the new-api and QuantumNous project or organization identities must not be modified, deleted, replaced, or removed." Modifying the Unicode escape sequence (\u0061) in the footer.new\u0061pi.projectAttributionSuffix key violates this protection mechanism.

  • web/default/src/i18n/locales/en.json#L2028-L2028: revert the key back to footer.new\u0061pi.projectAttributionSuffix.
  • web/default/src/i18n/locales/fr.json#L2028-L2028: revert the key back to footer.new\u0061pi.projectAttributionSuffix.
📍 Affects 2 files
  • web/default/src/i18n/locales/en.json#L2028-L2028 (this comment)
  • web/default/src/i18n/locales/fr.json#L2028-L2028
🤖 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/i18n/locales/en.json` at line 2028, Restore the protected
Unicode-escaped key footer.new\u0061pi.projectAttributionSuffix in both
web/default/src/i18n/locales/en.json at lines 2028-2028 and
web/default/src/i18n/locales/fr.json at lines 2028-2028; leave the translation
values unchanged.

Source: Coding guidelines

Comment on lines +5182 to +5186
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",
"Master switch for per-channel concurrency limits": "Interrupteur principal pour les limites de concurrence par canal",
"Max Concurrency": "Max Concurrency",
"Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate English text to French.

The newly added keys have English values in the French locale file. They should be translated into French.

📝 Proposed translations
-    "Channel Limit Enabled": "Channel Limit Enabled",
-    "Channel Limits": "Channel Limits",
-    "Master switch for per-channel concurrency limits": "Master switch for per-channel concurrency limits",
-    "Max Concurrency": "Max Concurrency",
-    "Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
+    "Channel Limit Enabled": "Limite de canal activée",
+    "Channel Limits": "Limites de canal",
+    "Master switch for per-channel concurrency limits": "Interrupteur principal pour les limites de simultanéité par canal",
+    "Max Concurrency": "Simultanéité maximale",
+    "Max concurrent requests to this channel (0 = unlimited)": "Nombre maximal de requêtes simultanées vers ce canal (0 = illimité)"
📝 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
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",
"Master switch for per-channel concurrency limits": "Interrupteur principal pour les limites de concurrence par canal",
"Max Concurrency": "Max Concurrency",
"Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
"Channel Limit Enabled": "Limite de canal activée",
"Channel Limits": "Limites de canal",
"Master switch for per-channel concurrency limits": "Interrupteur principal pour les limites de simultanéité par canal",
"Max Concurrency": "Simultanéité maximale",
"Max concurrent requests to this channel (0 = unlimited)": "Nombre maximal de requêtes simultanées vers ce canal (0 = illimité)"
🤖 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/i18n/locales/fr.json` around lines 5182 - 5186, Translate the
English values for “Channel Limit Enabled,” “Channel Limits,” “Max Concurrency,”
and “Max concurrent requests to this channel (0 = unlimited)” in the French
locale, while preserving the existing keys and JSON structure.

Comment on lines +5182 to +5186
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",
"Master switch for per-channel concurrency limits": "チャネルごとの同時実行制限のメインスイッチ",
"Max Concurrency": "Max Concurrency",
"Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Translate the new Japanese locale values.

These entries are user-facing, but the labels and description remain in English, so Japanese users will see untranslated concurrency settings.

Proposed fix
-    "Channel Limit Enabled": "Channel Limit Enabled",
-    "Channel Limits": "Channel Limits",
+    "Channel Limit Enabled": "チャネル制限を有効化",
+    "Channel Limits": "チャネル制限",
     "Master switch for per-channel concurrency limits": "チャネルごとの同時実行制限のメインスイッチ",
-    "Max Concurrency": "Max Concurrency",
-    "Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
+    "Max Concurrency": "最大同時実行数",
+    "Max concurrent requests to this channel (0 = unlimited)": "このチャネルへの最大同時リクエスト数(0 = 無制限)"
📝 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
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",
"Master switch for per-channel concurrency limits": "チャネルごとの同時実行制限のメインスイッチ",
"Max Concurrency": "Max Concurrency",
"Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
"Channel Limit Enabled": "チャネル制限を有効化",
"Channel Limits": "チャネル制限",
"Master switch for per-channel concurrency limits": "チャネルごとの同時実行制限のメインスイッチ",
"Max Concurrency": "最大同時実行数",
"Max concurrent requests to this channel (0 = unlimited)": "このチャネルへの最大同時リクエスト数(0 = 無制限)"
🤖 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/i18n/locales/ja.json` around lines 5182 - 5186, Translate the
user-facing values for “Channel Limit Enabled,” “Channel Limits,” “Max
Concurrency,” and “Max concurrent requests to this channel (0 = unlimited)” in
the Japanese locale, while preserving the existing translation for “Master
switch for per-channel concurrency limits.”

Sources: Coding guidelines, Learnings

Comment on lines +5182 to +5186
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",
"Master switch for per-channel concurrency limits": "Главный переключатель ограничений параллелизма канала",
"Max Concurrency": "Max Concurrency",
"Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the new concurrency-limit values.

Channel Limit Enabled, Channel Limits, Max Concurrency, and the maximum-request description remain English in ru.json, so Russian users will see incomplete localization.

Suggested translations
-    "Channel Limit Enabled": "Channel Limit Enabled",
-    "Channel Limits": "Channel Limits",
+    "Channel Limit Enabled": "Ограничение каналов включено",
+    "Channel Limits": "Лимиты каналов",
     "Master switch for per-channel concurrency limits": "Главный переключатель ограничений параллелизма канала",
-    "Max Concurrency": "Max Concurrency",
-    "Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
+    "Max Concurrency": "Максимальное число одновременных запросов",
+    "Max concurrent requests to this channel (0 = unlimited)": "Максимальное число одновременных запросов к этому каналу (0 = без ограничений)"
📝 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
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",
"Master switch for per-channel concurrency limits": "Главный переключатель ограничений параллелизма канала",
"Max Concurrency": "Max Concurrency",
"Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
"Channel Limit Enabled": "Ограничение каналов включено",
"Channel Limits": "Лимиты каналов",
"Master switch for per-channel concurrency limits": "Главный переключатель ограничений параллелизма канала",
"Max Concurrency": "Максимальное число одновременных запросов",
"Max concurrent requests to this channel (0 = unlimited)": "Максимальное число одновременных запросов к этому каналу (0 = без ограничений)"
🤖 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/i18n/locales/ru.json` around lines 5182 - 5186, Translate the
four newly added concurrency-limit entries in the Russian locale—“Channel Limit
Enabled”, “Channel Limits”, “Max Concurrency”, and “Max concurrent requests to
this channel (0 = unlimited)”—while preserving the existing Russian translation
for the master-switch description.

Comment on lines +5182 to +5183
"Channel Limit Enabled": "Channel Limit Enabled",
"Channel Limits": "Channel Limits",

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Translate the new Vietnamese locale values.

These entries currently render English text in the Vietnamese UI. Replace them with Vietnamese translations.

Proposed fix
-    "Channel Limit Enabled": "Channel Limit Enabled",
-    "Channel Limits": "Channel Limits",
+    "Channel Limit Enabled": "Bật giới hạn kênh",
+    "Channel Limits": "Giới hạn kênh",
     "Master switch for per-channel concurrency limits": "Công tắc chính cho giới hạn đồng thời mỗi kênh",
-    "Max Concurrency": "Max Concurrency",
-    "Max concurrent requests to this channel (0 = unlimited)": "Max concurrent requests to this channel (0 = unlimited)"
+    "Max Concurrency": "Số lượng đồng thời tối đa",
+    "Max concurrent requests to this channel (0 = unlimited)": "Số yêu cầu đồng thời tối đa đến kênh này (0 = không giới hạn)"

Also applies to: 5185-5186

🤖 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/i18n/locales/vi.json` around lines 5182 - 5183, Translate the
Vietnamese locale values for the “Channel Limit Enabled” and “Channel Limits”
keys in the locale data, replacing the English text while preserving the
existing keys and JSON structure.

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