Skip to content

feat: support multi-key channel key testing#5646

Closed
SwillerDawn wants to merge 2 commits into
QuantumNous:mainfrom
NaciTech:feat/key-level-channel-test-default
Closed

feat: support multi-key channel key testing#5646
SwillerDawn wants to merge 2 commits into
QuantumNous:mainfrom
NaciTech:feat/key-level-channel-test-default

Conversation

@SwillerDawn

@SwillerDawn SwillerDawn commented Jun 21, 2026

Copy link
Copy Markdown

⚠️ 提交说明 / PR Notice

Important

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

📝 变更描述 / Description

为多 Key 渠道的模型测试增加 Key 级别测试能力。
本次变更允许前端在渠道测试弹窗中为 multi-key 渠道选择指定 Key 进行模型测试;后端 /api/channel/test/:id 支持 key_index 参数,并在测试上下文中优先使用指定的启用 Key。若指定 Key 不可用或渠道不是 multi-key,会返回明确错误。
同时,测试失败且选择了具体 Key 时,前端提供“禁用当前密钥”操作,便于管理员快速处理异常 Key。默认“自动选择”仍保持原有随机/轮询选择逻辑,不影响普通渠道和自动测试流程。

🚀 变更类型 / 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

(请在此粘贴截图、关键日志或测试报告,以证明变更生效)
image

Summary by CodeRabbit

  • New Features

    • Added support for testing specific API keys in multi-key channels
    • Added key selection dropdown to the channel test dialog
    • Added ability to disable API keys directly from the test interface
    • Improved API key preview display using secure masking
  • Localization

    • Added translations for multi-key testing features in French, Japanese, Russian, Vietnamese, and Chinese

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds per-key targeting to the channel test flow: a new Channel.GetEnabledKeyByIndex model method, middleware branching to select a forced key index in SetupContextForSelectedChannel, an HTTP key_index query parameter in TestChannel, and a full frontend test-dialog overhaul with a key selector, composite result keys, and a "Disable current key" action. Also replaces KeyPreview truncation with model.MaskTokenKey.

Changes

Multi-key channel test targeting

Layer / File(s) Summary
Channel model: GetEnabledKeyByIndex and KeyPreview masking
model/channel.go, controller/channel.go
Adds GetEnabledKeyByIndex to retrieve and validate a specific enabled key by index. Replaces the first-10-chars KeyPreview truncation in ManageMultiKeys with model.MaskTokenKey.
Middleware key selection with forced index
middleware/distributor.go
Adds SetChannelTestMultiKeyIndex context helper and branches SetupContextForSelectedChannel to use the forced index or fall back to GetNextEnabledKey, returning ChannelNoAvailableKey errors for non-multi-key or unavailable keys.
HTTP handler key_index query param
controller/channel-test.go
testChannel gains an optional keyIndex *int parameter and injects it via SetChannelTestMultiKeyIndex; TestChannel parses and validates the key_index query param; testAllChannels passes nil.
Frontend API client and channel-actions keyIndex wiring
web/default/src/features/channels/api.ts, web/default/src/features/channels/lib/channel-actions.ts
Extends testChannel params type to include key_index and updates handleTestChannel options to accept keyIndex, mapping it into the outgoing request payload.
Test dialog: state, derived values, and composite result keys
web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
Adds React state for selected key index, key status list, and disable-loading flag; introduces getModelTestResultKey composite key generator; updates successModels/failedModels filtering to use composite keys.
Test dialog: loadKeyStatuses, disableSelectedTestKey, and test request flow
web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
Adds loadModelTestKeyStatuses and disableSelectedTestKey async handlers; updates single-model and batch-model test flows to read/write results under composite testResultKey; passes keyIndex into handleTestChannel; updates handleDeleteFailedModels to use composite keys.
Test dialog: key selector UI and failure-cell disable action
web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
Adds conditional Test Key select control; extends grid layout for multi-key channels; updates TestStatusCell and FailureStatusContent props with disable controls; renders destructive "Disable current key" button in failure cells; updates columns memo dependencies.
i18n strings for multi-key test UI
web/default/src/i18n/locales/*.json
Adds translation keys for Auto select, Test Key, Disable current key, Key disabled, and related error/prompt strings across en, zh, fr, ja, ru, vi locales; reorders Show … entries in each locale file.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TestDialog
    participant handleTestChannel
    participant GoTestChannel
    participant SetupContextForSelectedChannel
    participant GetEnabledKeyByIndex

    User->>TestDialog: Open dialog (multi-key channel)
    TestDialog->>GoTestChannel: GET /api/channel/multi-keys (getMultiKeyStatus)
    GoTestChannel-->>TestDialog: KeyStatus[] with masked KeyPreview

    User->>TestDialog: Select key from Test Key dropdown
    User->>TestDialog: Click test model

    TestDialog->>handleTestChannel: { keyIndex: selectedKeyIndex }
    handleTestChannel->>GoTestChannel: POST /api/channel/test?key_index=N

    GoTestChannel->>SetupContextForSelectedChannel: inject key index via SetChannelTestMultiKeyIndex
    SetupContextForSelectedChannel->>GetEnabledKeyByIndex: index N
    GetEnabledKeyByIndex-->>SetupContextForSelectedChannel: key string or error

    SetupContextForSelectedChannel-->>GoTestChannel: selected key
    GoTestChannel-->>handleTestChannel: test result
    handleTestChannel-->>TestDialog: testResults[composite key]
    TestDialog->>User: Show result + optional Disable current key button

    User->>TestDialog: Click Disable current key
    TestDialog->>GoTestChannel: disableMultiKey(channelId, keyIndex)
    GoTestChannel-->>TestDialog: updated key statuses
    TestDialog->>TestDialog: Refresh key list + reset selection
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • QuantumNous/new-api#1653: Both PRs modify controller/channel.go in the multi-key get_key_status response path, overlapping on the KeyPreview field in the KeyStatus struct.
  • QuantumNous/new-api#2875: Both PRs extend testChannel in controller/channel-test.go with new optional parameters and update call sites including testAllChannels.
  • QuantumNous/new-api#4988: Both PRs modify channel-test-dialog.tsx to change per-model test result state management and status cell rendering logic.

Suggested reviewers

  • seefs001

Poem

🐇 Hop, hop! A key parade,
Each key tested, none betrayed.
Pick your index, watch it spin,
Masked and safe — no peeking in!
Disable the bad, keep the good,
Multi-key works as it should! 🗝️

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: support multi-key channel key testing' accurately summarizes the main feature addition—enabling per-key testing capability for multi-key channels across both frontend and backend.
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.

✏️ 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.

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

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/dialogs/channel-test-dialog.tsx (2)

849-906: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not delete channel models from a specific-key failure set.

failed is now scoped to the selected key via getModelTestResultKey(model), but updateChannel(... { models }) removes those models globally from the channel. In specific-key mode, a failure usually means the key is bad, not the model; hide/guard this destructive path unless the selector is in auto mode.

Proposed guard
 const handleDeleteFailedModels = useCallback(async () => {
+  if (typeof selectedKeyIndex === 'number') {
+    setIsDeleteFailedDialogOpen(false)
+    return
+  }
+
   const failed = models.filter(
     (model) => testResults[getModelTestResultKey(model)]?.status === 'error'
   )
@@
   }, [
     currentRow.id,
     getModelTestResultKey,
     models,
     refreshChannelLists,
+    selectedKeyIndex,
     t,
     testResults,
   ])

Also hide the toolbar action in specific-key mode:

-                  {failedModels.length > 0 && (
+                  {failedModels.length > 0 &&
+                    typeof selectedKeyIndex !== 'number' && (
                     <Button
                       variant='outline'
                       size='sm'
                       onClick={() => setIsDeleteFailedDialogOpen(true)}
@@
                       })}
                     </Button>
-                  )}
+                    )}
🤖 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/dialogs/channel-test-dialog.tsx`
around lines 849 - 906, The handleDeleteFailedModels function currently allows
deletion of models globally based on test failures for a specific key, but in
specific-key mode this is problematic since a key failure doesn't mean the model
is bad. Add a guard condition at the beginning of handleDeleteFailedModels to
check if the selector is in auto mode before allowing the deletion to proceed;
if in specific-key mode, exit early by setting the dialog state and returning,
or consider disabling this action entirely in the UI when in specific-key mode.

981-1036: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not offer key disabling for model-price/configuration failures.

canDisableTestKey is true for every failed result, including MODEL_PRICE_ERROR_CODE, where the existing UI correctly points users to pricing settings. Showing “Disable current key” there can disable a healthy key for a non-key-specific configuration error.

Proposed gate
               canDisableTestKey={
                 isMultiKeyChannel &&
                 typeof selectedKeyIndex === 'number' &&
-                result?.status === 'error'
+                result?.status === 'error' &&
+                result.errorCode !== MODEL_PRICE_ERROR_CODE
               }
🤖 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/dialogs/channel-test-dialog.tsx`
around lines 981 - 1036, The `canDisableTestKey` property is currently enabled
for all error results, but it should exclude model-price and
configuration-specific failures where disabling the key is not the correct
resolution. Modify the condition for `canDisableTestKey` in the TestStatusCell
configuration to additionally check that the error result is not a
pricing-related error code (such as MODEL_PRICE_ERROR_CODE). You should gate the
key disabling option by verifying the specific error type in the result object,
ensuring users are not offered to disable a healthy key when the actual issue is
a configuration or pricing problem unrelated to the key itself.
🧹 Nitpick comments (1)
web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx (1)

553-621: Refactor the multi-key fetch and disable operations to use React Query hooks for consistency with the project pattern.

The loadModelTestKeyStatuses and disableSelectedTestKey functions should be wrapped with useQuery and useMutation respectively, using the established channelsQueryKeys pattern. This aligns with the project's React Query implementation elsewhere in the channels feature and provides automatic loading, error handling, and cache invalidation management per the coding guidelines.

🤖 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/dialogs/channel-test-dialog.tsx`
around lines 553 - 621, The loadModelTestKeyStatuses and disableSelectedTestKey
functions currently use manual state management with
setModelTestKeyStatusLoading and setDisableTestKeyLoading instead of React Query
hooks. Refactor loadModelTestKeyStatuses to use the useQuery hook with the
channelsQueryKeys pattern (following the project's React Query implementation),
and refactor disableSelectedTestKey to use the useMutation hook with appropriate
onSuccess callback for cache invalidation. This will align with the established
patterns in the channels feature and provide consistent loading state, error
handling, and automatic cache management without manual state management.

Source: Coding guidelines

🤖 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 `@model/channel.go`:
- Around line 295-300: The code at line 295 reads
channel.ChannelInfo.MultiKeyStatusList without acquiring the channel polling
lock, while other operations like GetNextEnabledKey and ManageMultiKeys use
GetChannelPollingLock(channel.Id) to protect concurrent access to this map. Wrap
the statusList access and the subsequent if block with
GetChannelPollingLock(channel.Id) to synchronize this read with other status
reads and writes, preventing potential race conditions and panic on concurrent
map access.

---

Outside diff comments:
In
`@web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx`:
- Around line 849-906: The handleDeleteFailedModels function currently allows
deletion of models globally based on test failures for a specific key, but in
specific-key mode this is problematic since a key failure doesn't mean the model
is bad. Add a guard condition at the beginning of handleDeleteFailedModels to
check if the selector is in auto mode before allowing the deletion to proceed;
if in specific-key mode, exit early by setting the dialog state and returning,
or consider disabling this action entirely in the UI when in specific-key mode.
- Around line 981-1036: The `canDisableTestKey` property is currently enabled
for all error results, but it should exclude model-price and
configuration-specific failures where disabling the key is not the correct
resolution. Modify the condition for `canDisableTestKey` in the TestStatusCell
configuration to additionally check that the error result is not a
pricing-related error code (such as MODEL_PRICE_ERROR_CODE). You should gate the
key disabling option by verifying the specific error type in the result object,
ensuring users are not offered to disable a healthy key when the actual issue is
a configuration or pricing problem unrelated to the key itself.

---

Nitpick comments:
In
`@web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx`:
- Around line 553-621: The loadModelTestKeyStatuses and disableSelectedTestKey
functions currently use manual state management with
setModelTestKeyStatusLoading and setDisableTestKeyLoading instead of React Query
hooks. Refactor loadModelTestKeyStatuses to use the useQuery hook with the
channelsQueryKeys pattern (following the project's React Query implementation),
and refactor disableSelectedTestKey to use the useMutation hook with appropriate
onSuccess callback for cache invalidation. This will align with the established
patterns in the channels feature and provide consistent loading state, error
handling, and automatic cache management without manual state management.
🪄 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: b2a5033c-b672-4e2e-9741-cfb0cfa82bf2

📥 Commits

Reviewing files that changed from the base of the PR and between 0b7ae4e and 0871a9c.

📒 Files selected for processing (13)
  • controller/channel-test.go
  • controller/channel.go
  • middleware/distributor.go
  • model/channel.go
  • web/default/src/features/channels/api.ts
  • web/default/src/features/channels/components/dialogs/channel-test-dialog.tsx
  • web/default/src/features/channels/lib/channel-actions.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 thread model/channel.go
Comment on lines +295 to +300
statusList := channel.ChannelInfo.MultiKeyStatusList
if statusList != nil {
if status, ok := statusList[index]; ok && status != common.ChannelStatusEnabled {
return "", index, fmt.Sprintf("key_status_%d", status), false
}
}

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 | ⚡ Quick win

Synchronize forced-index key-status reads with the channel lock.

Line 295 reads MultiKeyStatusList without GetChannelPollingLock(channel.Id). Other status reads/writes (GetNextEnabledKey, ManageMultiKeys) use this lock, so this path can race and panic on concurrent map access.

Proposed fix
 func (channel *Channel) GetEnabledKeyByIndex(index int) (string, int, string, bool) {
 	if index < 0 {
 		return "", index, "invalid_key_index", false
 	}
+
+	lock := GetChannelPollingLock(channel.Id)
+	lock.Lock()
+	defer lock.Unlock()
 
 	keys := channel.GetKeys()
 	if index >= len(keys) {
 		return "", index, "key_index_out_of_range", false
 	}
📝 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
statusList := channel.ChannelInfo.MultiKeyStatusList
if statusList != nil {
if status, ok := statusList[index]; ok && status != common.ChannelStatusEnabled {
return "", index, fmt.Sprintf("key_status_%d", status), false
}
}
func (channel *Channel) GetEnabledKeyByIndex(index int) (string, int, string, bool) {
if index < 0 {
return "", index, "invalid_key_index", false
}
lock := GetChannelPollingLock(channel.Id)
lock.Lock()
defer lock.Unlock()
keys := channel.GetKeys()
if index >= len(keys) {
return "", index, "key_index_out_of_range", false
}
statusList := channel.ChannelInfo.MultiKeyStatusList
if statusList != nil {
if status, ok := statusList[index]; ok && status != common.ChannelStatusEnabled {
return "", index, fmt.Sprintf("key_status_%d", status), false
}
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@model/channel.go` around lines 295 - 300, The code at line 295 reads
channel.ChannelInfo.MultiKeyStatusList without acquiring the channel polling
lock, while other operations like GetNextEnabledKey and ManageMultiKeys use
GetChannelPollingLock(channel.Id) to protect concurrent access to this map. Wrap
the statusList access and the subsequent if block with
GetChannelPollingLock(channel.Id) to synchronize this read with other status
reads and writes, preventing potential race conditions and panic on concurrent
map access.

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