Skip to content

Commit e710d97

Browse files
committed
Refactor model fetching in webviewMessageHandler to handle failures independently
- Introduced a helper function to safely fetch models, ensuring that one failure does not affect others. - Replaced Promise.all with Promise.allSettled to manage multiple model fetch requests concurrently while capturing errors.
1 parent cd02cd8 commit e710d97

File tree

2 files changed

+185
-14
lines changed

2 files changed

+185
-14
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// npx jest src/core/webview/__tests__/webviewMessageHandler.test.ts
2+
3+
import { webviewMessageHandler } from "../webviewMessageHandler"
4+
import { getModels } from "../../../api/providers/fetchers/modelCache"
5+
6+
// Mock dependencies
7+
jest.mock("../../../api/providers/fetchers/modelCache", () => ({
8+
getModels: jest.fn(),
9+
flushModels: jest.fn().mockResolvedValue(undefined),
10+
}))
11+
12+
describe("webviewMessageHandler", () => {
13+
// Set up provider mock with essential methods needed by the handler
14+
const mockProvider = {
15+
postMessageToWebview: jest.fn(),
16+
getState: jest.fn().mockResolvedValue({
17+
apiConfiguration: {
18+
openRouterApiKey: "mock-openrouter-key",
19+
requestyApiKey: "mock-requesty-key",
20+
glamaApiKey: "mock-glama-key",
21+
unboundApiKey: "mock-unbound-key",
22+
litellmApiKey: "mock-litellm-key",
23+
litellmBaseUrl: "https://mock-litellm-url",
24+
},
25+
}),
26+
log: jest.fn(),
27+
}
28+
29+
beforeEach(() => {
30+
jest.clearAllMocks()
31+
})
32+
33+
describe("requestRouterModels", () => {
34+
test("handles all successful model fetches correctly", async () => {
35+
// Mock all getModels calls to succeed with different data
36+
;(getModels as jest.Mock).mockImplementation((router) => {
37+
return Promise.resolve({
38+
[`${router}-model-1`]: { name: `${router} Model 1` },
39+
[`${router}-model-2`]: { name: `${router} Model 2` },
40+
})
41+
})
42+
43+
// Call the handler
44+
await webviewMessageHandler(mockProvider as any, {
45+
type: "requestRouterModels",
46+
})
47+
48+
// Verify the provider posted the correct message with all models
49+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
50+
type: "routerModels",
51+
routerModels: {
52+
openrouter: {
53+
"openrouter-model-1": { name: "openrouter Model 1" },
54+
"openrouter-model-2": { name: "openrouter Model 2" },
55+
},
56+
requesty: {
57+
"requesty-model-1": { name: "requesty Model 1" },
58+
"requesty-model-2": { name: "requesty Model 2" },
59+
},
60+
glama: {
61+
"glama-model-1": { name: "glama Model 1" },
62+
"glama-model-2": { name: "glama Model 2" },
63+
},
64+
unbound: {
65+
"unbound-model-1": { name: "unbound Model 1" },
66+
"unbound-model-2": { name: "unbound Model 2" },
67+
},
68+
litellm: {
69+
"litellm-model-1": { name: "litellm Model 1" },
70+
"litellm-model-2": { name: "litellm Model 2" },
71+
},
72+
},
73+
})
74+
})
75+
76+
test("handles some failed model fetches correctly", async () => {
77+
// Mock some getModels calls to succeed and others to fail
78+
;(getModels as jest.Mock).mockImplementation((router) => {
79+
if (router === "openrouter" || router === "litellm") {
80+
return Promise.resolve({
81+
[`${router}-model-1`]: { name: `${router} Model 1` },
82+
})
83+
}
84+
// For other routers, throw an error
85+
return Promise.reject(new Error(`Failed to fetch ${router} models`))
86+
})
87+
88+
// Call the handler
89+
await webviewMessageHandler(mockProvider as any, {
90+
type: "requestRouterModels",
91+
})
92+
93+
// Verify the provider posted the correct message with only successful models
94+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
95+
type: "routerModels",
96+
routerModels: {
97+
openrouter: {
98+
"openrouter-model-1": { name: "openrouter Model 1" },
99+
},
100+
requesty: {},
101+
glama: {},
102+
unbound: {},
103+
litellm: {
104+
"litellm-model-1": { name: "litellm Model 1" },
105+
},
106+
},
107+
})
108+
})
109+
110+
test("handles all failed model fetches correctly", async () => {
111+
// Mock all getModels calls to fail
112+
;(getModels as jest.Mock).mockRejectedValue(new Error("API Error"))
113+
114+
// Call the handler
115+
await webviewMessageHandler(mockProvider as any, {
116+
type: "requestRouterModels",
117+
})
118+
119+
// Verify the provider posted the correct message with empty objects for each router
120+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
121+
type: "routerModels",
122+
routerModels: {
123+
openrouter: {},
124+
requesty: {},
125+
glama: {},
126+
unbound: {},
127+
litellm: {},
128+
},
129+
})
130+
})
131+
})
132+
})

src/core/webview/webviewMessageHandler.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -318,23 +318,62 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
318318
case "requestRouterModels":
319319
const { apiConfiguration } = await provider.getState()
320320

321-
const [openRouterModels, requestyModels, glamaModels, unboundModels, litellmModels] = await Promise.all([
322-
getModels("openrouter", apiConfiguration.openRouterApiKey),
323-
getModels("requesty", apiConfiguration.requestyApiKey),
324-
getModels("glama", apiConfiguration.glamaApiKey),
325-
getModels("unbound", apiConfiguration.unboundApiKey),
326-
getModels("litellm", apiConfiguration.litellmApiKey, apiConfiguration.litellmBaseUrl),
327-
])
321+
// Handle each model fetch independently to avoid one failure affecting others
322+
const routerModels = {
323+
openrouter: {},
324+
requesty: {},
325+
glama: {},
326+
unbound: {},
327+
litellm: {},
328+
}
329+
330+
// Helper function to safely fetch models
331+
const safeGetModels = async (router: RouterName, apiKey?: string, baseUrl?: string) => {
332+
try {
333+
return await getModels(router, apiKey, baseUrl)
334+
} catch (error) {
335+
console.error(`Failed to fetch models for ${router}:`, error)
336+
return {} // Return empty object on failure
337+
}
338+
}
339+
340+
// Fetch all models in parallel but handle failures independently
341+
const results = await Promise.allSettled(
342+
[
343+
{ key: "openrouter", promise: safeGetModels("openrouter", apiConfiguration.openRouterApiKey) },
344+
{ key: "requesty", promise: safeGetModels("requesty", apiConfiguration.requestyApiKey) },
345+
{ key: "glama", promise: safeGetModels("glama", apiConfiguration.glamaApiKey) },
346+
{ key: "unbound", promise: safeGetModels("unbound", apiConfiguration.unboundApiKey) },
347+
{
348+
key: "litellm",
349+
promise: safeGetModels(
350+
"litellm",
351+
apiConfiguration.litellmApiKey,
352+
apiConfiguration.litellmBaseUrl,
353+
),
354+
},
355+
].map(async ({ key, promise }) => {
356+
try {
357+
const models = await promise
358+
return { key, models }
359+
} catch (error) {
360+
console.error(`Error in router models fetch for ${key}:`, error)
361+
return { key, models: {} }
362+
}
363+
}),
364+
)
365+
366+
// Process results and assign to routerModels
367+
results.forEach((result) => {
368+
if (result.status === "fulfilled") {
369+
const key = result.value.key as keyof typeof routerModels
370+
routerModels[key] = result.value.models
371+
}
372+
})
328373

329374
provider.postMessageToWebview({
330375
type: "routerModels",
331-
routerModels: {
332-
openrouter: openRouterModels,
333-
requesty: requestyModels,
334-
glama: glamaModels,
335-
unbound: unboundModels,
336-
litellm: litellmModels,
337-
},
376+
routerModels,
338377
})
339378
break
340379
case "requestOpenAiModels":

0 commit comments

Comments
 (0)