Skip to content

Commit 40331eb

Browse files
committed
feat: enhance OpenRouter model management with refresh functionality
✨ OpenRouter Model Refresh Features: - Add refresh button with multilingual support (16 languages) - Implement Node.js native https module to bypass Cloudflare blocking - Add auto-initialization of router models on startup - Optimize model fetching with graceful error handling 🌐 Multilingual Support: - Add 'refreshModels' translation key across all 16 supported languages - Maintain consistent UI experience across different locales 🚀 Performance Improvements: - Reduce redundant API calls with smart caching - Better error handling for network issues Based on upstream v3.18.3 with proper Git merge
1 parent 8da9858 commit 40331eb

File tree

20 files changed

+359
-58
lines changed

20 files changed

+359
-58
lines changed

src/api/providers/fetchers/openrouter.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import axios from "axios"
2+
import * as https from "https"
23
import { z } from "zod"
34

45
import { isModelParameter } from "../../../schemas"
@@ -96,8 +97,47 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
9697
const models: Record<string, ModelInfo> = {}
9798
const baseURL = options?.openRouterBaseUrl || "https://openrouter.ai/api/v1"
9899

100+
// Ensure baseURL uses HTTPS protocol
101+
const secureBaseURL = baseURL.startsWith("https://")
102+
? baseURL
103+
: baseURL.startsWith("http://")
104+
? baseURL.replace("http://", "https://")
105+
: `https://${baseURL}`
106+
99107
try {
100-
const response = await axios.get<OpenRouterModelsResponse>(`${baseURL}/models`)
108+
// Use Node.js native https module instead of axios
109+
const url = new URL(`${secureBaseURL}/models`)
110+
111+
const responseData = await new Promise<OpenRouterModelsResponse>((resolve, reject) => {
112+
const req = https.request(url, (res: any) => {
113+
let data = ""
114+
115+
res.on("data", (chunk: any) => {
116+
data += chunk
117+
})
118+
119+
res.on("end", () => {
120+
if (res.statusCode >= 200 && res.statusCode < 300) {
121+
try {
122+
const parsedData = JSON.parse(data)
123+
resolve(parsedData)
124+
} catch (error) {
125+
reject(new Error(`Failed to parse JSON response: ${error}`))
126+
}
127+
} else {
128+
reject(new Error(`Request failed with status code ${res.statusCode}`))
129+
}
130+
})
131+
})
132+
133+
req.on("error", (error: any) => {
134+
reject(error)
135+
})
136+
137+
req.end()
138+
})
139+
140+
const response = { data: responseData }
101141
const result = openRouterModelsResponseSchema.safeParse(response.data)
102142
const data = result.success ? result.data.data : response.data.data
103143

@@ -120,6 +160,16 @@ export async function getOpenRouterModels(options?: ApiHandlerOptions): Promise<
120160
console.error(
121161
`Error fetching OpenRouter models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
122162
)
163+
164+
// Add detailed error logging for axios errors
165+
if (axios.isAxiosError(error) && error.response) {
166+
console.error("OpenRouter API error response:", {
167+
status: error.response.status,
168+
statusText: error.response.statusText,
169+
data: error.response.data,
170+
headers: error.response.headers,
171+
})
172+
}
123173
}
124174

125175
return models

src/core/webview/webviewMessageHandler.ts

Lines changed: 153 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,115 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
117117
telemetryService.updateTelemetryState(isOptedIn)
118118
})
119119

120+
// Auto-request router models if API keys are configured
121+
// Add a slight delay to ensure the webview is fully initialized
122+
setTimeout(async () => {
123+
try {
124+
const state = await provider.getStateToPostToWebview()
125+
const { apiConfiguration } = state
126+
127+
if (
128+
apiConfiguration.openRouterApiKey ||
129+
apiConfiguration.requestyApiKey ||
130+
apiConfiguration.glamaApiKey ||
131+
apiConfiguration.unboundApiKey ||
132+
(apiConfiguration.litellmApiKey && apiConfiguration.litellmBaseUrl)
133+
) {
134+
// Request router models automatically on initialization
135+
provider.log("Auto-requesting router models on initialization")
136+
137+
// Use the same logic as requestRouterModels case
138+
const routerModels: Partial<Record<RouterName, ModelRecord>> = {
139+
openrouter: {},
140+
requesty: {},
141+
glama: {},
142+
unbound: {},
143+
litellm: {},
144+
}
145+
146+
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
147+
try {
148+
return await getModels(options)
149+
} catch (error) {
150+
console.error(
151+
`Failed to fetch models in auto-initialization for ${options.provider}:`,
152+
error,
153+
)
154+
return {}
155+
}
156+
}
157+
158+
const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = []
159+
160+
if (apiConfiguration.openRouterApiKey) {
161+
modelFetchPromises.push({ key: "openrouter", options: { provider: "openrouter" } })
162+
}
163+
164+
if (apiConfiguration.requestyApiKey) {
165+
modelFetchPromises.push({
166+
key: "requesty",
167+
options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey },
168+
})
169+
}
170+
171+
if (apiConfiguration.glamaApiKey) {
172+
modelFetchPromises.push({ key: "glama", options: { provider: "glama" } })
173+
}
174+
175+
if (apiConfiguration.unboundApiKey) {
176+
modelFetchPromises.push({
177+
key: "unbound",
178+
options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey },
179+
})
180+
}
181+
182+
if (apiConfiguration.litellmApiKey && apiConfiguration.litellmBaseUrl) {
183+
modelFetchPromises.push({
184+
key: "litellm",
185+
options: {
186+
provider: "litellm",
187+
apiKey: apiConfiguration.litellmApiKey,
188+
baseUrl: apiConfiguration.litellmBaseUrl,
189+
},
190+
})
191+
}
192+
193+
if (modelFetchPromises.length > 0) {
194+
const results = await Promise.allSettled(
195+
modelFetchPromises.map(async ({ key, options }) => {
196+
const models = await safeGetModels(options)
197+
return { key, models }
198+
}),
199+
)
200+
201+
const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = { ...routerModels }
202+
203+
results.forEach((result, index) => {
204+
const routerName = modelFetchPromises[index].key
205+
206+
if (result.status === "fulfilled") {
207+
fetchedRouterModels[routerName] = result.value.models
208+
} else {
209+
fetchedRouterModels[routerName] = {}
210+
}
211+
})
212+
213+
provider.log(
214+
`Auto-fetched router models for: ${modelFetchPromises.map((p) => p.key).join(", ")}`,
215+
)
216+
await provider.postMessageToWebview({
217+
type: "routerModels",
218+
routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
219+
})
220+
}
221+
}
222+
} catch (error) {
223+
provider.log(
224+
`Error auto-fetching router models: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
225+
)
226+
}
227+
}, 1000) // 1 second delay
228+
120229
provider.isViewLaunched = true
121230
break
122231
case "newTask":
@@ -289,75 +398,65 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
289398
case "requestRouterModels":
290399
const { apiConfiguration } = await provider.getState()
291400

292-
const routerModels: Partial<Record<RouterName, ModelRecord>> = {
293-
openrouter: {},
294-
requesty: {},
295-
glama: {},
296-
unbound: {},
297-
litellm: {},
298-
}
401+
// 只请求用户配置的提供商的模型列表
402+
const promises = []
403+
const routerNames = []
299404

300-
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
301-
try {
302-
return await getModels(options)
303-
} catch (error) {
304-
console.error(
305-
`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
306-
error,
307-
)
308-
throw error // Re-throw to be caught by Promise.allSettled
309-
}
405+
// 只有在用户配置了API密钥时才请求相应提供商的模型列表
406+
if (apiConfiguration.openRouterApiKey) {
407+
promises.push(getModels({ provider: "openrouter" }))
408+
routerNames.push("openrouter")
310409
}
311410

312-
const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
313-
{ key: "openrouter", options: { provider: "openrouter" } },
314-
{ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
315-
{ key: "glama", options: { provider: "glama" } },
316-
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
317-
]
411+
if (apiConfiguration.requestyApiKey) {
412+
promises.push(getModels({ provider: "requesty", apiKey: apiConfiguration.requestyApiKey }))
413+
routerNames.push("requesty")
414+
}
318415

319-
const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
320-
const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
321-
if (litellmApiKey && litellmBaseUrl) {
322-
modelFetchPromises.push({
323-
key: "litellm",
324-
options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },
325-
})
416+
if (apiConfiguration.glamaApiKey) {
417+
promises.push(getModels({ provider: "glama" }))
418+
routerNames.push("glama")
326419
}
327420

328-
const results = await Promise.allSettled(
329-
modelFetchPromises.map(async ({ key, options }) => {
330-
const models = await safeGetModels(options)
331-
return { key, models } // key is RouterName here
332-
}),
333-
)
421+
if (apiConfiguration.unboundApiKey) {
422+
promises.push(getModels({ provider: "unbound", apiKey: apiConfiguration.unboundApiKey }))
423+
routerNames.push("unbound")
424+
}
334425

335-
const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = { ...routerModels }
426+
if (apiConfiguration.litellmApiKey && apiConfiguration.litellmBaseUrl) {
427+
promises.push(
428+
getModels({
429+
provider: "litellm",
430+
apiKey: apiConfiguration.litellmApiKey,
431+
baseUrl: apiConfiguration.litellmBaseUrl,
432+
}),
433+
)
434+
routerNames.push("litellm")
435+
}
336436

337-
results.forEach((result, index) => {
338-
const routerName = modelFetchPromises[index].key // Get RouterName using index
437+
// Use Promise.allSettled to handle API failures gracefully
438+
const results = await Promise.allSettled(promises)
339439

340-
if (result.status === "fulfilled") {
341-
fetchedRouterModels[routerName] = result.value.models
342-
} else {
343-
// Handle rejection: Post a specific error message for this provider
344-
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
345-
console.error(`Error fetching models for ${routerName}:`, result.reason)
440+
// Extract results, using empty objects for any failed requests
441+
const modelResults = results.map((result) => (result.status === "fulfilled" ? result.value : {}))
346442

347-
fetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message
443+
// Build routerModels object with all providers
444+
const routerModels = {
445+
openrouter: {},
446+
requesty: {},
447+
glama: {},
448+
unbound: {},
449+
litellm: {},
450+
}
348451

349-
provider.postMessageToWebview({
350-
type: "singleRouterModelFetchResponse",
351-
success: false,
352-
error: errorMessage,
353-
values: { provider: routerName },
354-
})
355-
}
452+
// Assign results to corresponding providers
453+
routerNames.forEach((name, index) => {
454+
routerModels[name as RouterName] = modelResults[index]
356455
})
357456

358457
provider.postMessageToWebview({
359458
type: "routerModels",
360-
routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
459+
routerModels,
361460
})
362461
break
363462
case "requestOpenAiModels":

0 commit comments

Comments
 (0)