feat(web): fetch upstream model in model mapping#6108
Conversation
WalkthroughThe 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. ChangesUpstream model selection
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
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winUse 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 anaria-label, and keep the iconaria-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 | 🔵 TrivialVendor 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), andmodel.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 tradeoffConsider extracting upstream-model caching into a custom hook.
upstreamModelsCacheRef+mappingModelFetcherform a self-contained caching concern bolted onto an already very large component. Extracting them into auseUpstreamModelsCache(...)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 winSolid implementation; consider extracting shared vendor-category rendering.
The vendor-grouped
Collapsiblerendering here is structurally near-identical torenderModelCategoryinfetch-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 arenderControl(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
📒 Files selected for processing (6)
web/default/src/features/channels/components/dialogs/fetch-models-dialog.tsxweb/default/src/features/channels/components/dialogs/single-model-select-dialog.tsxweb/default/src/features/channels/components/drawers/channel-mutate-drawer.tsxweb/default/src/features/channels/components/model-mapping-editor.tsxweb/default/src/features/channels/lib/index.tsweb/default/src/features/channels/lib/model-vendor-category.ts
| <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> |
There was a problem hiding this comment.
🎯 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.tsxRepository: 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")
PYRepository: 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.
| // 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]) | ||
|
|
There was a problem hiding this comment.
🗄️ 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.
| // 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.
| 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 |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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 | ||
| } |
There was a problem hiding this comment.
🎯 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
Important
📝 变更描述 / Description
(简述:做了什么?为什么这样改能生效?请基于你对代码逻辑的理解来写,避免粘贴未经整理的内容)
在此PR之前,对Replacement Model的自动补全不符合实际使用场景,#6101
此PR新增了一个单选的模型选择窗口,以及一个缓存上游模型列表的fetcher,和一个前端的按钮,便于直接从上游拉取已有的模型列表并快速填入
🚀 变更类型 / Type of change
🔗 关联任务 / Related Issue
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
(请在此粘贴截图、关键日志或测试报告,以证明变更生效)


Summary by CodeRabbit
New Features
Improvements