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
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-17 07:12:38 -03:00
committed by GitHub
parent de9cfcd940
commit 2013103765
3 changed files with 95 additions and 0 deletions

View File

@@ -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)

View File

@@ -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<string, () => Array<{ id: string; name: str
],
};
const SEARCH_TYPE_LABELS: Record<string, string> = {
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 }>,

View File

@@ -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}"`
);
}
});