Skip to content

feat(web): fetch upstream model in model mapping#6108

Open
Bliod-Cook wants to merge 1 commit into
QuantumNous:mainfrom
Bliod-Cook:feat-model-mapping-upstream
Open

feat(web): fetch upstream model in model mapping#6108
Bliod-Cook wants to merge 1 commit into
QuantumNous:mainfrom
Bliod-Cook:feat-model-mapping-upstream

Conversation

@Bliod-Cook

@Bliod-Cook Bliod-Cook commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

⚠️ 提交说明 / PR Notice

Important

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

📝 变更描述 / Description

(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)

在此PR之前,对Replacement Model的自动补全不符合实际使用场景,#6101

此PR新增了一个单选的模型选择窗口,以及一个缓存上游模型列表的fetcher,和一个前端的按钮,便于直接从上游拉取已有的模型列表并快速填入

🚀 变更类型 / Type of change

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

🔗 关联任务 / Related Issue

✅ 提交前检查项 / Checklist

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

📸 运行证明 / Proof of Work

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

Summary by CodeRabbit

  • New Features

    • Added a searchable dialog for selecting a single upstream model.
    • Models are grouped and sorted by vendor for easier browsing.
    • Added model selection controls to model-mapping rows.
    • Added loading, empty-state, retry, and error feedback during model retrieval.
  • Improvements

    • Upstream model results are reused when possible to reduce repeated fetching.
    • Model names are normalized and de-duplicated.
    • Existing model-fetching dialogs now use consistent vendor-based grouping.

@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The change adds vendor-based model categorization, a reusable single-model selection dialog, and cached upstream model fetching for channel model mappings. Mapping rows can now select replacement models from upstream results.

Changes

Upstream model selection

Layer / File(s) Summary
Vendor grouping and fetch dialog
web/default/src/features/channels/lib/*, web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx
Adds shared vendor categorization and ordering helpers, then uses them in the existing model-fetching dialog with refactored computed content.
Single-model picker
web/default/src/features/channels/components/dialogs/single-model-select-dialog.tsx
Adds asynchronous loading, retry and error states, search, vendor grouping, radio selection, and confirmation for one model.
Mapping fetch integration
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx, web/default/src/features/channels/components/model-mapping-editor.tsx
Adds cached upstream fetching and wires mapping replacement inputs to the single-model picker.
Estimated code review effort: 3 (Moderate) ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ChannelMutateDrawer
  participant ModelMappingEditor
  participant SingleModelSelectDialog
  participant UpstreamModelFetcher
  ChannelMutateDrawer->>ModelMappingEditor: pass mappingModelFetcher
  ModelMappingEditor->>SingleModelSelectDialog: open replacement-model picker
  SingleModelSelectDialog->>ChannelMutateDrawer: request upstream models
  ChannelMutateDrawer->>UpstreamModelFetcher: fetch or reuse cached results
  UpstreamModelFetcher-->>SingleModelSelectDialog: return model names
  SingleModelSelectDialog-->>ModelMappingEditor: confirm selected model
  ModelMappingEditor-->>ChannelMutateDrawer: update mapping row
Loading

Suggested reviewers: seefs001

Poem

I’m a rabbit with models to choose,
From upstream burrows, fresh news.
Grouped by vendor, neat and bright,
Pick one replacement just right.
Cached hops make the workflow light!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: fetching upstream models for model mapping.
Linked Issues check ✅ Passed The PR implements the requested upstream model fetch button and single-select flow for replacement models in #6101.
Out of Scope Changes check ✅ Passed The changes stay focused on upstream model selection and supporting model-mapping UI behavior.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ 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: 4

Caution

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

⚠️ Outside diff range comments (1)
web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx (1)

303-311: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use a focusable trigger here. render={<Info />} makes the tooltip trigger an unfocusable SVG, so keyboard users can’t reach it and the icon has no accessible label. Wrap the icon in a button (or other focusable element) with an aria-label, and keep the icon aria-hidden="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/channels/components/dialogs/fetch-models-dialog.tsx`
around lines 303 - 311, Update the TooltipTrigger in the model redirect
indicator to render a focusable button containing the Info icon, add a
descriptive aria-label to the button, and mark the icon aria-hidden="true" so
keyboard users can access the tooltip.

Source: Coding guidelines

🧹 Nitpick comments (3)
web/default/src/features/channels/lib/model-vendor-category.ts (1)

32-52: 📐 Maintainability & Code Quality | 🔵 Trivial

Vendor keyword matching is fragile and repeats .toLowerCase().

Substring checks like includes('o1')/includes('o3') can misclassify unrelated model names (any name containing that literal 2-char sequence), and model.toLowerCase() is recomputed up to 9 times per model. Consider lowercasing once per model and using a data-driven {vendor, patterns} list (word-boundary/prefix-aware) for easier maintenance and to reduce misclassification risk.

🤖 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/lib/model-vendor-category.ts` around lines
32 - 52, Update the model vendor categorization logic in the relevant function
to lowercase the model name once, then evaluate a data-driven vendor/pattern
list instead of repeated substring checks. Make OpenAI matching boundary- or
prefix-aware so generic “o1”/“o3” sequences do not classify unrelated names,
while preserving existing vendor mappings and precedence.
web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx (1)

611-614: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Consider extracting upstream-model caching into a custom hook.

upstreamModelsCacheRef + mappingModelFetcher form a self-contained caching concern bolted onto an already very large component. Extracting them into a useUpstreamModelsCache(...) hook would isolate this logic and keep this file's growth in check.

Also applies to: 1444-1472

🤖 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 611 - 614, Extract the self-contained upstream-model caching logic
from the component into a useUpstreamModelsCache(...) custom hook, including
upstreamModelsCacheRef and mappingModelFetcher. Move the cache key, model
storage, fetch, and reuse behavior into the hook, then update the channel
mutation drawer to consume the hook while preserving its existing inputs and
results.

Source: Coding guidelines

web/default/src/features/channels/components/dialogs/single-model-select-dialog.tsx (1)

49-208: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Solid implementation; consider extracting shared vendor-category rendering.

The vendor-grouped Collapsible rendering here is structurally near-identical to renderModelCategory in fetch-models-dialog.tsx (same grouping/collapsible/grid layout, differing only in checkbox vs. radio selection control). Since both were introduced in this PR stack, extracting a shared <VendorCategoryList> component parameterized by a renderControl(model) callback would remove the duplication going forward.

🤖 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/single-model-select-dialog.tsx`
around lines 49 - 208, Extract the duplicated vendor-category rendering from
SingleModelSelectDialog and fetch-models-dialog.tsx into a shared
VendorCategoryList component. Parameterize it with the category entries and a
renderControl(model) callback so each dialog supplies its radio or checkbox
control, then replace both inline Collapsible/grid implementations while
preserving existing labels, IDs, selection behavior, and styling.
🤖 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
`@web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx`:
- Around line 391-449: Prevent the uncontrolled Tabs component from remounting
when selections or search results change. In the Tabs element, remove the key
based on activeChannel, fetchedModels.length, and removedModels.length, or
replace it with a stable fetch-generation identifier so checkbox toggles and
search updates preserve the active tab.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 1444-1472: Replace the delimiter-based cache key construction in
mappingModelFetcher with a collision-safe representation, such as serializing
the individual type, channelId, base_url, and key values as an ordered
array/object. Keep the existing cache lookup and assignment behavior unchanged
while ensuring distinct field combinations cannot produce the same key.
- Around line 1457-1462: Guard response.data with Array.isArray before assigning
it to models in the isEditing/channelId branch of the channel mutation logic;
use an empty array or the existing translated error path for non-array values,
matching fetch-models-dialog.tsx and ensuring SingleModelSelectDialog receives
only an array.

In `@web/default/src/features/channels/lib/model-vendor-category.ts`:
- Around line 23-61: Keep the internal `'Other'` category key in
categorizeModelsByVendor, but localize it at both React render call sites:
fetch-models-dialog.tsx and single-model-select-dialog.tsx. When rendering
category headers, pass t('Other') for the Other category, matching the existing
t('Removed') handling, while preserving other category labels and utility
behavior.

---

Outside diff comments:
In
`@web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx`:
- Around line 303-311: Update the TooltipTrigger in the model redirect indicator
to render a focusable button containing the Info icon, add a descriptive
aria-label to the button, and mark the icon aria-hidden="true" so keyboard users
can access the tooltip.

---

Nitpick comments:
In
`@web/default/src/features/channels/components/dialogs/single-model-select-dialog.tsx`:
- Around line 49-208: Extract the duplicated vendor-category rendering from
SingleModelSelectDialog and fetch-models-dialog.tsx into a shared
VendorCategoryList component. Parameterize it with the category entries and a
renderControl(model) callback so each dialog supplies its radio or checkbox
control, then replace both inline Collapsible/grid implementations while
preserving existing labels, IDs, selection behavior, and styling.

In
`@web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx`:
- Around line 611-614: Extract the self-contained upstream-model caching logic
from the component into a useUpstreamModelsCache(...) custom hook, including
upstreamModelsCacheRef and mappingModelFetcher. Move the cache key, model
storage, fetch, and reuse behavior into the hook, then update the channel
mutation drawer to consume the hook while preserving its existing inputs and
results.

In `@web/default/src/features/channels/lib/model-vendor-category.ts`:
- Around line 32-52: Update the model vendor categorization logic in the
relevant function to lowercase the model name once, then evaluate a data-driven
vendor/pattern list instead of repeated substring checks. Make OpenAI matching
boundary- or prefix-aware so generic “o1”/“o3” sequences do not classify
unrelated names, while preserving existing vendor mappings and precedence.
🪄 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: 9291790b-b459-461b-b2b5-2a93542c2bf1

📥 Commits

Reviewing files that changed from the base of the PR and between ad900bb and 6d84971.

📒 Files selected for processing (6)
  • web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx
  • web/default/src/features/channels/components/dialogs/single-model-select-dialog.tsx
  • web/default/src/features/channels/components/drawers/channel-mutate-drawer.tsx
  • web/default/src/features/channels/components/model-mapping-editor.tsx
  • web/default/src/features/channels/lib/index.ts
  • web/default/src/features/channels/lib/model-vendor-category.ts

Comment on lines +391 to +449
<Tabs
key={`${activeChannel?.id ?? 'custom'}-${fetchedModels.length}-${removedModels.length}`}
defaultValue={defaultTab}
>
<TabsList
className={`grid w-full ${removedModels.length > 0 ? 'grid-cols-3' : 'grid-cols-2'}`}
>
<TabsTrigger value='new' disabled={newModels.length === 0}>
{t('New Models ({{count}})', { count: newModels.length })}
</TabsTrigger>
<TabsTrigger
value='existing'
disabled={existingFilteredModels.length === 0}
>
{t('Existing Models ({{count}})', {
count: existingFilteredModels.length,
})}
</TabsTrigger>
{removedModels.length > 0 && (
<TabsTrigger value='removed'>
{t('Removed Models ({{count}})', {
count: removedModels.length,
})}
</TabsTrigger>
)}
</TabsList>

<TabsContent
value='new'
className='max-h-96 space-y-2 overflow-y-auto'
>
{getSortedVendorCategoryEntries(newModelsByCategory).map(
([category, models]) => renderModelCategory(category, models)
)}
</TabsContent>

<TabsContent
value='existing'
className='max-h-96 space-y-2 overflow-y-auto'
>
{getSortedVendorCategoryEntries(existingModelsByCategory).map(
([category, models]) => renderModelCategory(category, models)
)}
</TabsContent>

{removedModels.length > 0 && (
<TabsContent
value='removed'
className='max-h-96 space-y-2 overflow-y-auto'
>
<p className='text-muted-foreground text-xs'>
{t(
'These models are still in your selection but were not returned by the upstream listing. Entries that are only model_mapping source aliases are omitted. Toggle to adjust before saving.'
)}
</p>
{renderModelCategory(t('Removed'), removedModels)}
</TabsContent>
)}
</Tabs>

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the component and inspect relevant symbols first.
ast-grep outline web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx --view expanded

# Show the key state/derived data around the review target.
sed -n '1,220p' web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx
sed -n '220,520p' web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx

Repository: QuantumNous/new-api

Length of output: 16288


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the tab primitives to understand whether a key change resets uncontrolled state.
ast-grep outline web/default/src/components/design-system/tabs.tsx --view expanded
sed -n '1,240p' web/default/src/components/design-system/tabs.tsx

# Quick static probe: show exactly which inputs can affect the Tabs key here.
python3 - <<'PY'
from pathlib import Path
import re

text = Path("web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsx").read_text()
m = re.search(r'key=\{`([^`]+)`\}', text)
print("Tabs key template:", m.group(1) if m else "not found")
print("Dependencies that can alter removedModels length:")
print("- selectedModels via removedModels")
print("- searchKeyword via removedModels")
print("- fetchedModels via fetchedModelSet/removedModels")
PY

Repository: QuantumNous/new-api

Length of output: 2216


Prevent <Tabs> from remounting on selection/search changes.

removedModels.length changes when selectedModels or searchKeyword changes, so this uncontrolled <Tabs> remounts and resets the active tab back to defaultTab. Key it only off a stable fetch-generation value (or remove the key) so tab state survives checkbox toggles and searching.

🤖 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/fetch-models-dialog.tsx`
around lines 391 - 449, Prevent the uncontrolled Tabs component from remounting
when selections or search results change. In the Tabs element, remove the key
based on activeChannel, fetchedModels.length, and removedModels.length, or
replace it with a stable fetch-generation identifier so checkbox toggles and
search updates preserve the active tab.

Comment on lines +1444 to +1472
// Fetch upstream models for the model-mapping row picker, cached so
// consecutive row picks in one editing session reuse a single request.
const mappingModelFetcher = useCallback(async (): Promise<string[]> => {
const cacheKey = [
form.getValues('type'),
channelId ?? '',
form.getValues('base_url') || '',
form.getValues('key') || '',
].join('|')
if (upstreamModelsCacheRef.current?.key === cacheKey) {
return upstreamModelsCacheRef.current.models
}
let models: string[]
if (isEditing && channelId) {
const response = await fetchUpstreamModels(channelId)
if (!response.success || !response.data) {
throw new Error(response.message || t('Failed to fetch models'))
}
models = response.data
} else {
if (!form.getValues('key')?.trim()) {
throw new Error(t('Please enter API key first'))
}
models = await createModeFetcher()
}
upstreamModelsCacheRef.current = { key: cacheKey, models }
return models
}, [isEditing, channelId, createModeFetcher, form, t])

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.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Cache key can collide across different base_url/key combinations.

Joining fields with '|' (line 1452) is unsafe because the AWS key format used elsewhere in this same component embeds '|' in the key itself (AccessKey|SecretAccessKey|Region). Two distinct (base_url, key) pairs can produce an identical joined string (e.g. base_url='a|b', key='c' vs. base_url='a', key='b|c'), causing a stale/incorrect model list to be served from cache for a channel it wasn't fetched for.

🔧 Proposed fix
-    const cacheKey = [
-      form.getValues('type'),
-      channelId ?? '',
-      form.getValues('base_url') || '',
-      form.getValues('key') || '',
-    ].join('|')
+    const cacheKey = JSON.stringify([
+      form.getValues('type'),
+      channelId ?? '',
+      form.getValues('base_url') || '',
+      form.getValues('key') || '',
+    ])
📝 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
// Fetch upstream models for the model-mapping row picker, cached so
// consecutive row picks in one editing session reuse a single request.
const mappingModelFetcher = useCallback(async (): Promise<string[]> => {
const cacheKey = [
form.getValues('type'),
channelId ?? '',
form.getValues('base_url') || '',
form.getValues('key') || '',
].join('|')
if (upstreamModelsCacheRef.current?.key === cacheKey) {
return upstreamModelsCacheRef.current.models
}
let models: string[]
if (isEditing && channelId) {
const response = await fetchUpstreamModels(channelId)
if (!response.success || !response.data) {
throw new Error(response.message || t('Failed to fetch models'))
}
models = response.data
} else {
if (!form.getValues('key')?.trim()) {
throw new Error(t('Please enter API key first'))
}
models = await createModeFetcher()
}
upstreamModelsCacheRef.current = { key: cacheKey, models }
return models
}, [isEditing, channelId, createModeFetcher, form, t])
// Fetch upstream models for the model-mapping row picker, cached so
// consecutive row picks in one editing session reuse a single request.
const mappingModelFetcher = useCallback(async (): Promise<string[]> => {
const cacheKey = JSON.stringify([
form.getValues('type'),
channelId ?? '',
form.getValues('base_url') || '',
form.getValues('key') || '',
])
if (upstreamModelsCacheRef.current?.key === cacheKey) {
return upstreamModelsCacheRef.current.models
}
let models: string[]
if (isEditing && channelId) {
const response = await fetchUpstreamModels(channelId)
if (!response.success || !response.data) {
throw new Error(response.message || t('Failed to fetch models'))
}
models = response.data
} else {
if (!form.getValues('key')?.trim()) {
throw new Error(t('Please enter API key first'))
}
models = await createModeFetcher()
}
upstreamModelsCacheRef.current = { key: cacheKey, models }
return models
}, [isEditing, channelId, createModeFetcher, form, t])
🤖 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 1444 - 1472, Replace the delimiter-based cache key construction in
mappingModelFetcher with a collision-safe representation, such as serializing
the individual type, channelId, base_url, and key values as an ordered
array/object. Keep the existing cache lookup and assignment behavior unchanged
while ensuring distinct field combinations cannot produce the same key.

Comment on lines +1457 to +1462
if (isEditing && channelId) {
const response = await fetchUpstreamModels(channelId)
if (!response.success || !response.data) {
throw new Error(response.message || t('Failed to fetch models'))
}
models = response.data

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Missing Array.isArray guard on response.data.

Unlike the equivalent call in fetch-models-dialog.tsx (Array.isArray(response.data) ? response.data : []), this branch trusts response.data to already be an array once truthy. If the backend ever returns a non-array truthy value, this gets cached and later crashes SingleModelSelectDialog's list.map(...), which is caught but surfaces a raw JS error message to the user instead of a clean one.

🛡️ Proposed fix
-      const response = await fetchUpstreamModels(channelId)
-      if (!response.success || !response.data) {
-        throw new Error(response.message || t('Failed to fetch models'))
-      }
-      models = response.data
+      const response = await fetchUpstreamModels(channelId)
+      if (!response.success || !Array.isArray(response.data)) {
+        throw new Error(response.message || t('Failed to fetch models'))
+      }
+      models = response.data
📝 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
if (isEditing && channelId) {
const response = await fetchUpstreamModels(channelId)
if (!response.success || !response.data) {
throw new Error(response.message || t('Failed to fetch models'))
}
models = response.data
if (isEditing && channelId) {
const response = await fetchUpstreamModels(channelId)
if (!response.success || !Array.isArray(response.data)) {
throw new Error(response.message || t('Failed to fetch models'))
}
models = response.data
🤖 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 1457 - 1462, Guard response.data with Array.isArray before
assigning it to models in the isEditing/channelId branch of the channel mutation
logic; use an empty array or the existing translated error path for non-array
values, matching fetch-models-dialog.tsx and ensuring SingleModelSelectDialog
receives only an array.

Comment on lines +23 to +61
export function categorizeModelsByVendor(
models: string[]
): Record<string, string[]> {
const categories: Record<string, string[]> = {}

models.forEach((model) => {
let category = 'Other'

// Determine category based on model name
if (
model.toLowerCase().includes('gpt') ||
model.toLowerCase().includes('o1') ||
model.toLowerCase().includes('o3')
) {
category = 'OpenAI'
} else if (model.toLowerCase().includes('claude')) {
category = 'Anthropic'
} else if (model.toLowerCase().includes('gemini')) {
category = 'Gemini'
} else if (model.toLowerCase().includes('qwen')) {
category = 'Qwen'
} else if (model.toLowerCase().includes('deepseek')) {
category = 'DeepSeek'
} else if (model.toLowerCase().includes('glm')) {
category = 'Zhipu'
} else if (model.toLowerCase().includes('llama')) {
category = 'Meta'
} else if (model.toLowerCase().includes('mistral')) {
category = 'Mistral'
}

if (!categories[category]) {
categories[category] = []
}
categories[category].push(model)
})

return categories
}

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

'Other' category label bypasses i18n.

The 'Other' fallback category is a raw literal that flows straight into JSX at both call sites (fetch-models-dialog.tsx category header, single-model-select-dialog.tsx category header) without going through t(), unlike the 'Removed' category which is explicitly localized (renderModelCategory(t('Removed'), removedModels)). Since this is non-React utility code, keep 'Other' as an internal key here and localize it at the render call sites instead.

🌐 Proposed fix (at call sites)
-          <span className='font-medium'>
-            {categoryName} ({categoryModels.length})
-          </span>
+          <span className='font-medium'>
+            {categoryName === 'Other' ? t('Other') : categoryName} ({categoryModels.length})
+          </span>

As per coding guidelines, "User-facing React UI text must use useTranslation() and t('English key')."

🤖 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/lib/model-vendor-category.ts` around lines
23 - 61, Keep the internal `'Other'` category key in categorizeModelsByVendor,
but localize it at both React render call sites: fetch-models-dialog.tsx and
single-model-select-dialog.tsx. When rendering category headers, pass t('Other')
for the Other category, matching the existing t('Removed') handling, while
preserving other category labels and utility behavior.

Source: Coding guidelines

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