From 217ab4d2c355bc4b0b983d28baf5d24be6d213b5 Mon Sep 17 00:00:00 2001
From: Wu Tianwei <30284043+WTW0313@users.noreply.github.com>
Date: Tue, 28 Apr 2026 14:55:10 +0800
Subject: [PATCH] fix(web): filter model selector by model name (#35624)
---
.../model-selector/__tests__/popup.spec.tsx | 159 +++++++++++++++++-
.../model-selector/popup.tsx | 30 +++-
2 files changed, 179 insertions(+), 10 deletions(-)
diff --git a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx
index a440313b3c..42232a71c0 100644
--- a/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx
+++ b/web/app/components/header/account-setting/model-provider-page/model-selector/__tests__/popup.spec.tsx
@@ -55,7 +55,14 @@ vi.mock('../../hooks', async () => {
})
vi.mock('../popup-item', () => ({
- default: ({ model }: { model: Model }) =>
{model.provider}
,
+ default: ({ model }: { model: Model }) => (
+
+ {model.provider}
+ {model.models.map(modelItem => (
+ {modelItem.model}
+ ))}
+
+ ),
}))
vi.mock('@/context/provider-context', () => ({
@@ -207,6 +214,156 @@ describe('Popup', () => {
expect((input as HTMLInputElement).value).toBe('')
})
+ it('should show matching models when searching by model name', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'claude' } },
+ )
+
+ expect(screen.queryByText('openai')).not.toBeInTheDocument()
+ expect(screen.getByText('anthropic')).toBeInTheDocument()
+ expect(screen.getByText('claude-3')).toBeInTheDocument()
+ expect(screen.queryByText('gpt-4')).not.toBeInTheDocument()
+ expect(screen.queryByText('No model found for \u201Cclaude\u201D')).not.toBeInTheDocument()
+ })
+
+ it('should show empty search placeholder when no provider or model name matches', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'mistral' } },
+ )
+
+ expect(screen.getByText('No model found for \u201Cmistral\u201D'))!.toBeInTheDocument()
+ expect(screen.queryByText('openai')).not.toBeInTheDocument()
+ expect(screen.queryByText('gpt-4')).not.toBeInTheDocument()
+ })
+
+ it('should show all models of a provider when searching by provider label', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'openai' } },
+ )
+
+ expect(screen.getByText('openai'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-4'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-4o'))!.toBeInTheDocument()
+ expect(screen.queryByText('anthropic')).not.toBeInTheDocument()
+ expect(screen.queryByText('claude-3')).not.toBeInTheDocument()
+ })
+
+ it('should match by model provider key when model label does not contain the search text', () => {
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'openai' } },
+ )
+
+ expect(screen.getByText('azure_openai'))!.toBeInTheDocument()
+ expect(screen.getByText('gpt-4'))!.toBeInTheDocument()
+ })
+
+ it('should still apply scope features when matching by provider label', () => {
+ mockSupportFunctionCall.mockReturnValue(false)
+
+ renderPopup(
+ ,
+ )
+
+ fireEvent.change(
+ screen.getByPlaceholderText('datasetSettings.form.searchModel'),
+ { target: { value: 'openai' } },
+ )
+
+ expect(screen.getByText('No model found for \u201Copenai\u201D'))!.toBeInTheDocument()
+ expect(screen.queryByText('gpt-4')).not.toBeInTheDocument()
+ expect(screen.queryByText('gpt-4-tool')).not.toBeInTheDocument()
+ })
+
it('should not show compatible-only helper text when no scope features are applied', () => {
renderPopup(
= ({
}, [aiCreditVisibleProviders, installedProviderMap, modelList])
const filteredModelList = useMemo(() => {
+ const normalizedSearch = searchText.toLowerCase()
+ const matchesLabel = (label: Record) => {
+ if (label[language] !== undefined)
+ return label[language].toLowerCase().includes(normalizedSearch)
+ return Object.values(label).some(value =>
+ value.toLowerCase().includes(normalizedSearch),
+ )
+ }
+
const filtered = installedModelList.map((model) => {
- const matchesProviderSearch = !searchText
- || model.provider.toLowerCase().includes(searchText.toLowerCase())
- || Object.values(model.label).some(label => label.toLowerCase().includes(searchText.toLowerCase()))
+ const providerMatched = !!searchText && (
+ matchesLabel(model.label)
+ || model.provider.toLowerCase().includes(normalizedSearch)
+ )
const filteredModels = model.models
.filter((modelItem) => {
- if (modelItem.label[language] !== undefined)
- return modelItem.label[language].toLowerCase().includes(searchText.toLowerCase())
- return Object.values(modelItem.label).some(label =>
- label.toLowerCase().includes(searchText.toLowerCase()),
- )
+ if (!searchText || providerMatched)
+ return true
+ return matchesLabel(modelItem.label)
})
.filter((modelItem) => {
if (scopeFeatures.length === 0)
@@ -165,8 +173,12 @@ const Popup: FC = ({
return modelItem.features?.includes(feature) ?? false
})
})
- if (!matchesProviderSearch || (filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider)))
+ if (
+ (searchText && filteredModels.length === 0)
+ || (!searchText && filteredModels.length === 0 && !aiCreditVisibleProviders.has(model.provider))
+ ) {
return null
+ }
return { ...model, models: filteredModels }
}).filter((model): model is Model => model !== null)