From 20131037651fb1920d05c662804f5678c1d33ab1 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 17 Jul 2026 07:12:38 -0300 Subject: [PATCH] fix(providers): derive static model catalogs for search providers from searchTypes (#7589) getStaticModelsForProvider() only defined literal catalogs for linkup-search, ollama-search, and searchapi-search out of the 12 ids in SEARCH_PROVIDERS. The other 9 (serper-search, brave-search, perplexity-search, exa-search, tavily-search, google-pse-search, youcom-search, searxng-search, zai-search) returned undefined and hit the 400 "does not support models listing" tail in the models route during the "Import Models" step. Instead of adding 9 more one-off literal entries, generalize the class: when a provider has no dedicated STATIC_MODEL_PROVIDERS entry, fall back to a catalog derived from SEARCH_PROVIDERS[id].searchTypes (every search-registry entry already declares this). Future search providers added to searchRegistry.ts automatically get a usable catalog with zero extra code. Closes #7529 --- .../fixes/7529-search-static-catalog.md | 1 + src/lib/providers/staticModels.ts | 37 ++++++++++++ ...roviders-static-model-catalog-7529.test.ts | 57 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 changelog.d/fixes/7529-search-static-catalog.md create mode 100644 tests/unit/search-providers-static-model-catalog-7529.test.ts diff --git a/changelog.d/fixes/7529-search-static-catalog.md b/changelog.d/fixes/7529-search-static-catalog.md new file mode 100644 index 0000000000..3387691b81 --- /dev/null +++ b/changelog.d/fixes/7529-search-static-catalog.md @@ -0,0 +1 @@ +- fix(providers): search providers now expose a static model catalog derived from `searchTypes`, fixing "does not support models listing" 400 for serper-search, brave-search, perplexity-search, exa-search, tavily-search, google-pse-search, youcom-search, searxng-search, zai-search (#7529) diff --git a/src/lib/providers/staticModels.ts b/src/lib/providers/staticModels.ts index 88bb6d85d8..cb03319d24 100644 --- a/src/lib/providers/staticModels.ts +++ b/src/lib/providers/staticModels.ts @@ -8,6 +8,7 @@ import { } from "@omniroute/open-sse/config/audioRegistry.ts"; import { ANTIGRAVITY_PUBLIC_MODELS } from "@omniroute/open-sse/config/antigravityModelAliases.ts"; import { getStaticQoderModels } from "@omniroute/open-sse/services/qoderCli.ts"; +import { getSearchProvider } from "@omniroute/open-sse/config/searchRegistry.ts"; import { getModelsByProviderId } from "@/shared/constants/models"; @@ -115,12 +116,48 @@ const STATIC_MODEL_PROVIDERS: Record Array<{ id: string; name: str ], }; +const SEARCH_TYPE_LABELS: Record = { + web: "Web Search", + news: "News Search", +}; + +function formatSearchTypeLabel(searchType: string): string { + return ( + SEARCH_TYPE_LABELS[searchType] ?? + `${searchType.charAt(0).toUpperCase()}${searchType.slice(1)} Search` + ); +} + +/** + * Search providers don't have "models" — a provider IS the model (see + * open-sse/config/searchRegistry.ts header doc). Any search provider without a + * dedicated literal entry above (custom depth/engine catalog, e.g. + * "linkup-search") still needs a non-empty static catalog so the "Available + * Models" / model-import UI shows a usable list instead of a 400 "does not + * support models listing" (#7529). Derive it generically from the registry's + * own `searchTypes` so any *future* search provider is covered automatically. + */ +function getSearchProviderFallbackCatalog(provider: string): LocalCatalogModel[] | undefined { + const searchProvider = getSearchProvider(provider); + if (!searchProvider || searchProvider.searchTypes.length === 0) return undefined; + + return searchProvider.searchTypes.map((searchType) => ({ + id: searchType, + name: formatSearchTypeLabel(searchType), + })); +} + export function getStaticModelsForProvider(provider: string): LocalCatalogModel[] | undefined { const staticModelsFn = STATIC_MODEL_PROVIDERS[provider]; if (staticModelsFn) { return staticModelsFn(); } + const searchFallback = getSearchProviderFallbackCatalog(provider); + if (searchFallback) { + return searchFallback; + } + const specialtyModels: LocalCatalogModel[] = []; const appendModels = ( models: Array<{ id: string; name?: string }>, diff --git a/tests/unit/search-providers-static-model-catalog-7529.test.ts b/tests/unit/search-providers-static-model-catalog-7529.test.ts new file mode 100644 index 0000000000..85efe28819 --- /dev/null +++ b/tests/unit/search-providers-static-model-catalog-7529.test.ts @@ -0,0 +1,57 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { SEARCH_PROVIDERS } from "@omniroute/open-sse/config/searchRegistry.ts"; +import { getStaticModelsForProvider } from "@/lib/providers/staticModels"; + +const EXCLUDED_FROM_ISSUE = new Set(["duckduckgo-free"]); + +const AFFECTED_PER_ISSUE = [ + "serper-search", + "brave-search", + "perplexity-search", + "exa-search", + "tavily-search", + "google-pse-search", + "youcom-search", + "searxng-search", + "zai-search", +]; + +test("#7529 — every SEARCH_PROVIDERS id should have a static model catalog (RED until fixed)", () => { + const searchProviderIds = Object.keys(SEARCH_PROVIDERS).filter( + (id) => !EXCLUDED_FROM_ISSUE.has(id) + ); + + for (const id of AFFECTED_PER_ISSUE) { + assert.ok(searchProviderIds.includes(id), `expected ${id} to still be present in SEARCH_PROVIDERS`); + } + + const missing: string[] = []; + for (const id of searchProviderIds) { + const catalog = getStaticModelsForProvider(id); + if (!catalog || catalog.length === 0) missing.push(id); + } + + assert.deepEqual( + missing.sort(), + [], + `search providers with NO static model catalog (will 400 "does not support models listing" on import): ${missing.join(", ")}` + ); +}); + +test("#7529 — a brand-new SEARCH_PROVIDERS entry with no literal STATIC_MODEL_PROVIDERS override still gets a usable catalog derived from searchTypes (generalized fix, not whack-a-mole)", () => { + // serper-search has no dedicated STATIC_MODEL_PROVIDERS["serper-search"] entry — + // this proves the fallback path (derived from SEARCH_PROVIDERS[id].searchTypes) + // is what supplies its catalog, not a one-off literal added for this issue. + const config = SEARCH_PROVIDERS["serper-search"]; + const catalog = getStaticModelsForProvider("serper-search"); + assert.ok(catalog && catalog.length > 0, "expected a static catalog for serper-search"); + const catalogIds = new Set((catalog ?? []).map((model) => model.id)); + for (const searchType of config.searchTypes) { + assert.ok( + catalogIds.has(searchType), + `expected the generalized catalog for serper-search to include its declared searchType "${searchType}"` + ); + } +});