feat: Add provider-scoped router model fetches in frontend

Implements intelligent provider filtering in the frontend to request
only the models needed for the currently selected provider. This
dramatically reduces memory usage by avoiding unnecessary fetches
and smaller cache footprints.

- Add DYNAMIC_ROUTER_PROVIDERS set to identify providers needing models
- Disable useRouterModels for static providers (anthropic, openai-native, etc.)
- Pass provider-specific filters for dynamic providers
- Add observability logging to track request/response patterns
- Update tests for new behavior

Works with backend filtering (PR #8916) to achieve end-to-end
payload reduction.
This commit is contained in:
daniel-lxs 2025-10-29 15:19:52 -05:00
parent ff0c65af10
commit dcdd631587
No known key found for this signature in database
GPG key ID: 21C74479048B3AA6
3 changed files with 82 additions and 25 deletions

View file

@ -291,7 +291,7 @@ describe("useSelectedModel", () => {
})
describe("loading and error states", () => {
it("should return loading state when router models are loading", () => {
it("should NOT set loading when router models are loading but provider is static (anthropic)", () => {
mockUseRouterModels.mockReturnValue({
data: undefined,
isLoading: true,
@ -307,10 +307,11 @@ describe("useSelectedModel", () => {
const wrapper = createWrapper()
const { result } = renderHook(() => useSelectedModel(), { wrapper })
expect(result.current.isLoading).toBe(true)
// With static provider default (anthropic), useSelectedModel gates router fetches, so loading should be false
expect(result.current.isLoading).toBe(false)
})
it("should return loading state when open router model providers are loading", () => {
it("should NOT set loading when openrouter provider metadata is loading but provider is static (anthropic)", () => {
mockUseRouterModels.mockReturnValue({
data: { openrouter: {}, requesty: {}, glama: {}, unbound: {}, litellm: {}, "io-intelligence": {} },
isLoading: false,
@ -326,10 +327,11 @@ describe("useSelectedModel", () => {
const wrapper = createWrapper()
const { result } = renderHook(() => useSelectedModel(), { wrapper })
expect(result.current.isLoading).toBe(true)
// With static provider default (anthropic), openrouter providers are irrelevant, so loading should be false
expect(result.current.isLoading).toBe(false)
})
it("should return error state when either hook has an error", () => {
it("should NOT set error when hooks error but provider is static (anthropic)", () => {
mockUseRouterModels.mockReturnValue({
data: undefined,
isLoading: false,
@ -345,7 +347,8 @@ describe("useSelectedModel", () => {
const wrapper = createWrapper()
const { result } = renderHook(() => useSelectedModel(), { wrapper })
expect(result.current.isError).toBe(true)
// Error from gated routerModels should not bubble for static provider default
expect(result.current.isError).toBe(false)
})
})

View file

@ -5,8 +5,16 @@ import { ExtensionMessage } from "@roo/ExtensionMessage"
import { vscode } from "@src/utils/vscode"
const getRouterModels = async () =>
type UseRouterModelsOptions = {
providers?: string[] // subset filter (e.g. ["roo"])
enabled?: boolean // gate fetching entirely
}
let __routerModelsRequestCount = 0
const getRouterModels = async (providers?: string[]) =>
new Promise<RouterModels>((resolve, reject) => {
const requestId = ++__routerModelsRequestCount
const cleanup = () => {
window.removeEventListener("message", handler)
}
@ -24,6 +32,10 @@ const getRouterModels = async () =>
cleanup()
if (message.routerModels) {
const keys = Object.keys(message.routerModels || {})
console.debug(
`[useRouterModels] response #${requestId} providers=${JSON.stringify(providers || "all")} keys=${keys.join(",")}`,
)
resolve(message.routerModels)
} else {
reject(new Error("No router models in response"))
@ -32,7 +44,21 @@ const getRouterModels = async () =>
}
window.addEventListener("message", handler)
vscode.postMessage({ type: "requestRouterModels" })
console.debug(
`[useRouterModels] request #${requestId} providers=${JSON.stringify(providers && providers.length ? providers : "all")}`,
)
if (providers && providers.length > 0) {
vscode.postMessage({ type: "requestRouterModels", values: { providers } })
} else {
vscode.postMessage({ type: "requestRouterModels" })
}
})
export const useRouterModels = () => useQuery({ queryKey: ["routerModels"], queryFn: getRouterModels })
export const useRouterModels = (opts: UseRouterModelsOptions = {}) => {
const providers = opts.providers && opts.providers.length ? [...opts.providers] : undefined
return useQuery({
queryKey: ["routerModels", providers?.slice().sort().join(",") || "all"],
queryFn: () => getRouterModels(providers),
enabled: opts.enabled !== false,
})
}

View file

@ -67,30 +67,56 @@ import { useOpenRouterModelProviders } from "./useOpenRouterModelProviders"
import { useLmStudioModels } from "./useLmStudioModels"
import { useOllamaModels } from "./useOllamaModels"
const DYNAMIC_ROUTER_PROVIDERS = new Set<ProviderName>([
"openrouter",
"vercel-ai-gateway",
"litellm",
"deepinfra",
"io-intelligence",
"requesty",
"unbound",
"glama",
"roo",
])
export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
const provider = apiConfiguration?.apiProvider || "anthropic"
const openRouterModelId = provider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined
const ollamaModelId = provider === "ollama" ? apiConfiguration?.ollamaModelId : undefined
const routerModels = useRouterModels()
// Only fetch router models for dynamic router providers we actually need
const shouldFetchRouterModels = DYNAMIC_ROUTER_PROVIDERS.has(provider as ProviderName)
const routerModels = useRouterModels({
providers: shouldFetchRouterModels ? [provider] : undefined,
enabled: shouldFetchRouterModels, // disable entirely for static providers
})
const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId)
const lmStudioModels = useLmStudioModels(lmStudioModelId)
const ollamaModels = useOllamaModels(ollamaModelId)
// Compute readiness only for the data actually needed for the selected provider
const needRouterModels = shouldFetchRouterModels
const needOpenRouterProviders = provider === "openrouter"
const needLmStudio = typeof lmStudioModelId !== "undefined"
const needOllama = typeof ollamaModelId !== "undefined"
const isReady =
(!needLmStudio || typeof lmStudioModels.data !== "undefined") &&
(!needOllama || typeof ollamaModels.data !== "undefined") &&
(!needRouterModels || typeof routerModels.data !== "undefined") &&
(!needOpenRouterProviders || typeof openRouterModelProviders.data !== "undefined")
const { id, info } =
apiConfiguration &&
(typeof lmStudioModelId === "undefined" || typeof lmStudioModels.data !== "undefined") &&
(typeof ollamaModelId === "undefined" || typeof ollamaModels.data !== "undefined") &&
typeof routerModels.data !== "undefined" &&
typeof openRouterModelProviders.data !== "undefined"
apiConfiguration && isReady
? getSelectedModel({
provider,
apiConfiguration,
routerModels: routerModels.data,
openRouterModelProviders: openRouterModelProviders.data,
lmStudioModels: lmStudioModels.data,
ollamaModels: ollamaModels.data,
routerModels: (routerModels.data || ({} as RouterModels)) as RouterModels,
openRouterModelProviders: (openRouterModelProviders.data || {}) as Record<string, ModelInfo>,
lmStudioModels: (lmStudioModels.data || undefined) as ModelRecord | undefined,
ollamaModels: (ollamaModels.data || undefined) as ModelRecord | undefined,
})
: { id: anthropicDefaultModelId, info: undefined }
@ -99,13 +125,15 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
id,
info,
isLoading:
routerModels.isLoading ||
openRouterModelProviders.isLoading ||
(apiConfiguration?.lmStudioModelId && lmStudioModels!.isLoading),
(needRouterModels && routerModels.isLoading) ||
(needOpenRouterProviders && openRouterModelProviders.isLoading) ||
(needLmStudio && lmStudioModels!.isLoading) ||
(needOllama && ollamaModels!.isLoading),
isError:
routerModels.isError ||
openRouterModelProviders.isError ||
(apiConfiguration?.lmStudioModelId && lmStudioModels!.isError),
(needRouterModels && routerModels.isError) ||
(needOpenRouterProviders && openRouterModelProviders.isError) ||
(needLmStudio && lmStudioModels!.isError) ||
(needOllama && ollamaModels!.isError),
}
}