Skip to content

Commit dcdd631

Browse files
committed
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.
1 parent ff0c65a commit dcdd631

File tree

3 files changed

+82
-25
lines changed

3 files changed

+82
-25
lines changed

webview-ui/src/components/ui/hooks/__tests__/useSelectedModel.spec.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ describe("useSelectedModel", () => {
291291
})
292292

293293
describe("loading and error states", () => {
294-
it("should return loading state when router models are loading", () => {
294+
it("should NOT set loading when router models are loading but provider is static (anthropic)", () => {
295295
mockUseRouterModels.mockReturnValue({
296296
data: undefined,
297297
isLoading: true,
@@ -307,10 +307,11 @@ describe("useSelectedModel", () => {
307307
const wrapper = createWrapper()
308308
const { result } = renderHook(() => useSelectedModel(), { wrapper })
309309

310-
expect(result.current.isLoading).toBe(true)
310+
// With static provider default (anthropic), useSelectedModel gates router fetches, so loading should be false
311+
expect(result.current.isLoading).toBe(false)
311312
})
312313

313-
it("should return loading state when open router model providers are loading", () => {
314+
it("should NOT set loading when openrouter provider metadata is loading but provider is static (anthropic)", () => {
314315
mockUseRouterModels.mockReturnValue({
315316
data: { openrouter: {}, requesty: {}, glama: {}, unbound: {}, litellm: {}, "io-intelligence": {} },
316317
isLoading: false,
@@ -326,10 +327,11 @@ describe("useSelectedModel", () => {
326327
const wrapper = createWrapper()
327328
const { result } = renderHook(() => useSelectedModel(), { wrapper })
328329

329-
expect(result.current.isLoading).toBe(true)
330+
// With static provider default (anthropic), openrouter providers are irrelevant, so loading should be false
331+
expect(result.current.isLoading).toBe(false)
330332
})
331333

332-
it("should return error state when either hook has an error", () => {
334+
it("should NOT set error when hooks error but provider is static (anthropic)", () => {
333335
mockUseRouterModels.mockReturnValue({
334336
data: undefined,
335337
isLoading: false,
@@ -345,7 +347,8 @@ describe("useSelectedModel", () => {
345347
const wrapper = createWrapper()
346348
const { result } = renderHook(() => useSelectedModel(), { wrapper })
347349

348-
expect(result.current.isError).toBe(true)
350+
// Error from gated routerModels should not bubble for static provider default
351+
expect(result.current.isError).toBe(false)
349352
})
350353
})
351354

webview-ui/src/components/ui/hooks/useRouterModels.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,16 @@ import { ExtensionMessage } from "@roo/ExtensionMessage"
55

66
import { vscode } from "@src/utils/vscode"
77

8-
const getRouterModels = async () =>
8+
type UseRouterModelsOptions = {
9+
providers?: string[] // subset filter (e.g. ["roo"])
10+
enabled?: boolean // gate fetching entirely
11+
}
12+
13+
let __routerModelsRequestCount = 0
14+
15+
const getRouterModels = async (providers?: string[]) =>
916
new Promise<RouterModels>((resolve, reject) => {
17+
const requestId = ++__routerModelsRequestCount
1018
const cleanup = () => {
1119
window.removeEventListener("message", handler)
1220
}
@@ -24,6 +32,10 @@ const getRouterModels = async () =>
2432
cleanup()
2533

2634
if (message.routerModels) {
35+
const keys = Object.keys(message.routerModels || {})
36+
console.debug(
37+
`[useRouterModels] response #${requestId} providers=${JSON.stringify(providers || "all")} keys=${keys.join(",")}`,
38+
)
2739
resolve(message.routerModels)
2840
} else {
2941
reject(new Error("No router models in response"))
@@ -32,7 +44,21 @@ const getRouterModels = async () =>
3244
}
3345

3446
window.addEventListener("message", handler)
35-
vscode.postMessage({ type: "requestRouterModels" })
47+
console.debug(
48+
`[useRouterModels] request #${requestId} providers=${JSON.stringify(providers && providers.length ? providers : "all")}`,
49+
)
50+
if (providers && providers.length > 0) {
51+
vscode.postMessage({ type: "requestRouterModels", values: { providers } })
52+
} else {
53+
vscode.postMessage({ type: "requestRouterModels" })
54+
}
3655
})
3756

38-
export const useRouterModels = () => useQuery({ queryKey: ["routerModels"], queryFn: getRouterModels })
57+
export const useRouterModels = (opts: UseRouterModelsOptions = {}) => {
58+
const providers = opts.providers && opts.providers.length ? [...opts.providers] : undefined
59+
return useQuery({
60+
queryKey: ["routerModels", providers?.slice().sort().join(",") || "all"],
61+
queryFn: () => getRouterModels(providers),
62+
enabled: opts.enabled !== false,
63+
})
64+
}

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,56 @@ import { useOpenRouterModelProviders } from "./useOpenRouterModelProviders"
6767
import { useLmStudioModels } from "./useLmStudioModels"
6868
import { useOllamaModels } from "./useOllamaModels"
6969

70+
const DYNAMIC_ROUTER_PROVIDERS = new Set<ProviderName>([
71+
"openrouter",
72+
"vercel-ai-gateway",
73+
"litellm",
74+
"deepinfra",
75+
"io-intelligence",
76+
"requesty",
77+
"unbound",
78+
"glama",
79+
"roo",
80+
])
81+
7082
export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
7183
const provider = apiConfiguration?.apiProvider || "anthropic"
7284
const openRouterModelId = provider === "openrouter" ? apiConfiguration?.openRouterModelId : undefined
7385
const lmStudioModelId = provider === "lmstudio" ? apiConfiguration?.lmStudioModelId : undefined
7486
const ollamaModelId = provider === "ollama" ? apiConfiguration?.ollamaModelId : undefined
7587

76-
const routerModels = useRouterModels()
88+
// Only fetch router models for dynamic router providers we actually need
89+
const shouldFetchRouterModels = DYNAMIC_ROUTER_PROVIDERS.has(provider as ProviderName)
90+
const routerModels = useRouterModels({
91+
providers: shouldFetchRouterModels ? [provider] : undefined,
92+
enabled: shouldFetchRouterModels, // disable entirely for static providers
93+
})
94+
7795
const openRouterModelProviders = useOpenRouterModelProviders(openRouterModelId)
7896
const lmStudioModels = useLmStudioModels(lmStudioModelId)
7997
const ollamaModels = useOllamaModels(ollamaModelId)
8098

99+
// Compute readiness only for the data actually needed for the selected provider
100+
const needRouterModels = shouldFetchRouterModels
101+
const needOpenRouterProviders = provider === "openrouter"
102+
const needLmStudio = typeof lmStudioModelId !== "undefined"
103+
const needOllama = typeof ollamaModelId !== "undefined"
104+
105+
const isReady =
106+
(!needLmStudio || typeof lmStudioModels.data !== "undefined") &&
107+
(!needOllama || typeof ollamaModels.data !== "undefined") &&
108+
(!needRouterModels || typeof routerModels.data !== "undefined") &&
109+
(!needOpenRouterProviders || typeof openRouterModelProviders.data !== "undefined")
110+
81111
const { id, info } =
82-
apiConfiguration &&
83-
(typeof lmStudioModelId === "undefined" || typeof lmStudioModels.data !== "undefined") &&
84-
(typeof ollamaModelId === "undefined" || typeof ollamaModels.data !== "undefined") &&
85-
typeof routerModels.data !== "undefined" &&
86-
typeof openRouterModelProviders.data !== "undefined"
112+
apiConfiguration && isReady
87113
? getSelectedModel({
88114
provider,
89115
apiConfiguration,
90-
routerModels: routerModels.data,
91-
openRouterModelProviders: openRouterModelProviders.data,
92-
lmStudioModels: lmStudioModels.data,
93-
ollamaModels: ollamaModels.data,
116+
routerModels: (routerModels.data || ({} as RouterModels)) as RouterModels,
117+
openRouterModelProviders: (openRouterModelProviders.data || {}) as Record<string, ModelInfo>,
118+
lmStudioModels: (lmStudioModels.data || undefined) as ModelRecord | undefined,
119+
ollamaModels: (ollamaModels.data || undefined) as ModelRecord | undefined,
94120
})
95121
: { id: anthropicDefaultModelId, info: undefined }
96122

@@ -99,13 +125,15 @@ export const useSelectedModel = (apiConfiguration?: ProviderSettings) => {
99125
id,
100126
info,
101127
isLoading:
102-
routerModels.isLoading ||
103-
openRouterModelProviders.isLoading ||
104-
(apiConfiguration?.lmStudioModelId && lmStudioModels!.isLoading),
128+
(needRouterModels && routerModels.isLoading) ||
129+
(needOpenRouterProviders && openRouterModelProviders.isLoading) ||
130+
(needLmStudio && lmStudioModels!.isLoading) ||
131+
(needOllama && ollamaModels!.isLoading),
105132
isError:
106-
routerModels.isError ||
107-
openRouterModelProviders.isError ||
108-
(apiConfiguration?.lmStudioModelId && lmStudioModels!.isError),
133+
(needRouterModels && routerModels.isError) ||
134+
(needOpenRouterProviders && openRouterModelProviders.isError) ||
135+
(needLmStudio && lmStudioModels!.isError) ||
136+
(needOllama && ollamaModels!.isError),
109137
}
110138
}
111139

0 commit comments

Comments
 (0)