Skip to content

Commit ff30f59

Browse files
committed
Litellm models can now be refreshed
1 parent b8aa4b4 commit ff30f59

File tree

29 files changed

+334
-116
lines changed

29 files changed

+334
-116
lines changed

src/api/providers/fetchers/litellm.ts

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { COMPUTER_USE_MODELS, ModelRecord } from "../../../shared/api"
77
* @param apiKey The API key for the LiteLLM server
88
* @param baseUrl The base URL of the LiteLLM server
99
* @returns A promise that resolves to a record of model IDs to model info
10+
* @throws Will throw an error if the request fails or the response is not as expected.
1011
*/
1112
export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise<ModelRecord> {
1213
try {
@@ -17,8 +18,8 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise
1718
if (apiKey) {
1819
headers["Authorization"] = `Bearer ${apiKey}`
1920
}
20-
21-
const response = await axios.get(`${baseUrl}/v1/model/info`, { headers })
21+
// Added timeout to prevent indefinite hanging
22+
const response = await axios.get(`${baseUrl}/v1/model/info`, { headers, timeout: 5000 })
2223
const models: ModelRecord = {}
2324

2425
const computerModels = Array.from(COMPUTER_USE_MODELS)
@@ -48,11 +49,25 @@ export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise
4849
description: `${modelName} via LiteLLM proxy`,
4950
}
5051
}
52+
} else {
53+
// If response.data.data is not in the expected format, consider it an error.
54+
console.error("Error fetching LiteLLM models: Unexpected response format", response.data)
55+
throw new Error("Failed to fetch LiteLLM models: Unexpected response format.")
5156
}
5257

5358
return models
54-
} catch (error) {
55-
console.error("Error fetching LiteLLM models:", error)
56-
return {}
59+
} catch (error: any) {
60+
console.error("Error fetching LiteLLM models:", error.message ? error.message : error)
61+
if (axios.isAxiosError(error) && error.response) {
62+
throw new Error(
63+
`Failed to fetch LiteLLM models: ${error.response.status} ${error.response.statusText}. Check base URL and API key.`,
64+
)
65+
} else if (axios.isAxiosError(error) && error.request) {
66+
throw new Error(
67+
"Failed to fetch LiteLLM models: No response from server. Check LiteLLM server status and base URL.",
68+
)
69+
} else {
70+
throw new Error(`Failed to fetch LiteLLM models: ${error.message || "An unknown error occurred."}`)
71+
}
5772
}
5873
}

src/api/providers/fetchers/modelCache.ts

Lines changed: 42 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { getRequestyModels } from "./requesty"
1313
import { getGlamaModels } from "./glama"
1414
import { getUnboundModels } from "./unbound"
1515
import { getLiteLLMModels } from "./litellm"
16-
16+
import { GetModelsOptions } from "../../../shared/api"
1717
const memoryCache = new NodeCache({ stdTTL: 5 * 60, checkperiod: 5 * 60 })
1818

1919
async function writeModels(router: RouterName, data: ModelRecord) {
@@ -41,64 +41,58 @@ async function readModels(router: RouterName): Promise<ModelRecord | undefined>
4141
* @param baseUrl - Optional base URL for the provider (currently used only for LiteLLM).
4242
* @returns The models from the cache or the fetched models.
4343
*/
44-
export const getModels = async (
45-
router: RouterName,
46-
apiKey: string | undefined = undefined,
47-
baseUrl: string | undefined = undefined,
48-
): Promise<ModelRecord> => {
49-
let models = memoryCache.get<ModelRecord>(router)
50-
44+
export const getModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
45+
const { provider } = options
46+
let models = memoryCache.get<ModelRecord>(provider)
5147
if (models) {
52-
// console.log(`[getModels] NodeCache hit for ${router} -> ${Object.keys(models).length}`)
5348
return models
5449
}
5550

56-
switch (router) {
57-
case "openrouter":
58-
models = await getOpenRouterModels()
59-
break
60-
case "requesty":
61-
// Requesty models endpoint requires an API key for per-user custom policies
62-
models = await getRequestyModels(apiKey)
63-
break
64-
case "glama":
65-
models = await getGlamaModels()
66-
break
67-
case "unbound":
68-
// Unbound models endpoint requires an API key to fetch application specific models
69-
models = await getUnboundModels(apiKey)
70-
break
71-
case "litellm":
72-
if (apiKey && baseUrl) {
73-
models = await getLiteLLMModels(apiKey, baseUrl)
74-
} else {
75-
models = {}
76-
}
77-
break
78-
}
51+
try {
52+
switch (provider) {
53+
case "openrouter":
54+
models = await getOpenRouterModels()
55+
break
56+
case "requesty":
57+
// Requesty models endpoint requires an API key for per-user custom policies
58+
models = await getRequestyModels(options.apiKey)
59+
break
60+
case "glama":
61+
models = await getGlamaModels()
62+
break
63+
case "unbound":
64+
// Unbound models endpoint requires an API key to fetch application specific models
65+
models = await getUnboundModels(options.apiKey)
66+
break
67+
case "litellm":
68+
// Type safety ensures apiKey and baseUrl are always provided for litellm
69+
models = await getLiteLLMModels(options.apiKey, options.baseUrl)
70+
break
71+
default:
72+
// Ensures router is exhaustively checked if RouterName is a strict union
73+
const exhaustiveCheck: never = provider
74+
throw new Error(`Unknown provider: ${exhaustiveCheck}`)
75+
}
7976

80-
if (Object.keys(models).length > 0) {
81-
// console.log(`[getModels] API fetch for ${router} -> ${Object.keys(models).length}`)
82-
memoryCache.set(router, models)
77+
// Cache the fetched models (even if empty, to signify a successful fetch with no models)
78+
memoryCache.set(provider, models)
79+
await writeModels(provider, models).catch((err) =>
80+
console.error(`[getModels] Error writing ${provider} models to file cache:`, err),
81+
)
8382

8483
try {
85-
await writeModels(router, models)
86-
// console.log(`[getModels] wrote ${router} models to file cache`)
84+
models = await readModels(provider)
85+
// console.log(`[getModels] read ${router} models from file cache`)
8786
} catch (error) {
88-
console.error(`[getModels] error writing ${router} models to file cache`, error)
87+
console.error(`[getModels] error reading ${provider} models from file cache`, error)
8988
}
90-
91-
return models
92-
}
93-
94-
try {
95-
models = await readModels(router)
96-
// console.log(`[getModels] read ${router} models from file cache`)
89+
return models || {}
9790
} catch (error) {
98-
console.error(`[getModels] error reading ${router} models from file cache`, error)
99-
}
91+
// Log the error and re-throw it so the caller can handle it (e.g., show a UI message).
92+
console.error(`[getModels] Failed to fetch models in modelCache for ${provider}:`, error)
10093

101-
return models ?? {}
94+
throw error // Re-throw the original error to be handled by the caller.
95+
}
10296
}
10397

10498
/**

src/api/providers/openrouter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
171171

172172
public async fetchModel() {
173173
const [models, endpoints] = await Promise.all([
174-
getModels("openrouter"),
174+
getModels({ provider: "openrouter" }),
175175
getModelEndpoints({
176176
router: "openrouter",
177177
modelId: this.options.openRouterModelId,

src/api/providers/requesty.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
4545
}
4646

4747
public async fetchModel() {
48-
this.models = await getModels("requesty")
48+
this.models = await getModels({ provider: "requesty" })
4949
return this.getModel()
5050
}
5151

src/api/providers/router-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export abstract class RouterProvider extends BaseProvider {
4444
}
4545

4646
public async fetchModel() {
47-
this.models = await getModels(this.name, this.client.apiKey, this.client.baseURL)
47+
this.models = await getModels({ provider: this.name, apiKey: this.client.apiKey, baseUrl: this.client.baseURL })
4848
return this.getModel()
4949
}
5050

src/core/webview/webviewMessageHandler.ts

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as vscode from "vscode"
66
import { ClineProvider } from "./ClineProvider"
77
import { Language, ProviderSettings, GlobalState, Package } from "../../schemas"
88
import { changeLanguage, t } from "../../i18n"
9-
import { RouterName, toRouterName } from "../../shared/api"
9+
import { RouterName, toRouterName, ModelRecord } from "../../shared/api"
1010
import { supportPrompt } from "../../shared/support-prompt"
1111
import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
1212
import { checkExistKey } from "../../shared/checkExistApiConfig"
@@ -32,6 +32,7 @@ import { TelemetrySetting } from "../../shared/TelemetrySetting"
3232
import { getWorkspacePath } from "../../utils/path"
3333
import { Mode, defaultModeSlug } from "../../shared/modes"
3434
import { getModels, flushModels } from "../../api/providers/fetchers/modelCache"
35+
import { GetModelsOptions } from "../../shared/api"
3536
import { generateSystemPrompt } from "./generateSystemPrompt"
3637
import { getCommand } from "../../utils/commands"
3738

@@ -170,10 +171,6 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
170171
case "askResponse":
171172
provider.getCurrentCline()?.handleWebviewAskResponse(message.askResponse!, message.text, message.images)
172173
break
173-
case "autoCondenseContextPercent":
174-
await updateGlobalState("autoCondenseContextPercent", message.value)
175-
await provider.postStateToWebview()
176-
break
177174
case "terminalOperation":
178175
if (message.terminalOperation) {
179176
provider.getCurrentCline()?.handleTerminalOperation(message.terminalOperation)
@@ -282,29 +279,81 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We
282279
await provider.resetState()
283280
break
284281
case "flushRouterModels":
285-
const routerName: RouterName = toRouterName(message.text)
286-
await flushModels(routerName)
282+
const routerNameFlush: RouterName = toRouterName(message.text)
283+
await flushModels(routerNameFlush)
287284
break
288285
case "requestRouterModels":
289286
const { apiConfiguration } = await provider.getState()
290287

291-
const [openRouterModels, requestyModels, glamaModels, unboundModels, litellmModels] = await Promise.all([
292-
getModels("openrouter", apiConfiguration.openRouterApiKey),
293-
getModels("requesty", apiConfiguration.requestyApiKey),
294-
getModels("glama", apiConfiguration.glamaApiKey),
295-
getModels("unbound", apiConfiguration.unboundApiKey),
296-
getModels("litellm", apiConfiguration.litellmApiKey, apiConfiguration.litellmBaseUrl),
297-
])
288+
const routerModels: Partial<Record<RouterName, ModelRecord>> = {
289+
openrouter: {},
290+
requesty: {},
291+
glama: {},
292+
unbound: {},
293+
litellm: {},
294+
}
295+
296+
const safeGetModels = async (options: GetModelsOptions): Promise<ModelRecord> => {
297+
try {
298+
return await getModels(options)
299+
} catch (error) {
300+
console.error(
301+
`Failed to fetch models in webviewMessageHandler requestRouterModels for ${options.provider}:`,
302+
error,
303+
)
304+
throw error // Re-throw to be caught by Promise.allSettled
305+
}
306+
}
307+
308+
const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
309+
{ key: "openrouter", options: { provider: "openrouter" } },
310+
{ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
311+
{ key: "glama", options: { provider: "glama" } },
312+
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
313+
]
314+
315+
const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
316+
const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
317+
if (litellmApiKey && litellmBaseUrl) {
318+
modelFetchPromises.push({
319+
key: "litellm",
320+
options: { provider: "litellm", apiKey: litellmApiKey, baseUrl: litellmBaseUrl },
321+
})
322+
}
323+
324+
const results = await Promise.allSettled(
325+
modelFetchPromises.map(async ({ key, options }) => {
326+
const models = await safeGetModels(options)
327+
return { key, models } // key is RouterName here
328+
}),
329+
)
330+
331+
const fetchedRouterModels: Partial<Record<RouterName, ModelRecord>> = { ...routerModels }
332+
333+
results.forEach((result, index) => {
334+
const routerName = modelFetchPromises[index].key // Get RouterName using index
335+
336+
if (result.status === "fulfilled") {
337+
fetchedRouterModels[routerName] = result.value.models
338+
} else {
339+
// Handle rejection: Post a specific error message for this provider
340+
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason)
341+
console.error(`Error fetching models for ${routerName}:`, result.reason)
342+
343+
fetchedRouterModels[routerName] = {} // Ensure it's an empty object in the main routerModels message
344+
345+
provider.postMessageToWebview({
346+
type: "singleRouterModelFetchResponse",
347+
success: false,
348+
error: errorMessage,
349+
values: { provider: routerName },
350+
})
351+
}
352+
})
298353

299354
provider.postMessageToWebview({
300355
type: "routerModels",
301-
routerModels: {
302-
openrouter: openRouterModels,
303-
requesty: requestyModels,
304-
glama: glamaModels,
305-
unbound: unboundModels,
306-
litellm: litellmModels,
307-
},
356+
routerModels: fetchedRouterModels as Record<RouterName, ModelRecord>,
308357
})
309358
break
310359
case "requestOpenAiModels":

src/shared/ExtensionMessage.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface ExtensionMessage {
7070
| "commandExecutionStatus"
7171
| "vsCodeSetting"
7272
| "condenseTaskContextResponse"
73+
| "singleRouterModelFetchResponse"
7374
text?: string
7475
action?:
7576
| "chatButtonClicked"

src/shared/api.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1166,7 +1166,7 @@ export const unboundDefaultModelInfo: ModelInfo = {
11661166

11671167
// LiteLLM
11681168
// https://docs.litellm.ai/
1169-
export const litellmDefaultModelId = "anthropic/claude-3-7-sonnet-20250219"
1169+
export const litellmDefaultModelId = "claude-3-7-sonnet-20250219"
11701170
export const litellmDefaultModelInfo: ModelInfo = {
11711171
maxTokens: 8192,
11721172
contextWindow: 200_000,
@@ -1832,3 +1832,15 @@ export function toRouterName(value?: string): RouterName {
18321832
export type ModelRecord = Record<string, ModelInfo>
18331833

18341834
export type RouterModels = Record<RouterName, ModelRecord>
1835+
1836+
/**
1837+
* Options for fetching models from different providers.
1838+
* This is a discriminated union type where the provider property determines
1839+
* which other properties are required.
1840+
*/
1841+
export type GetModelsOptions =
1842+
| { provider: "openrouter" }
1843+
| { provider: "glama" }
1844+
| { provider: "requesty"; apiKey?: string }
1845+
| { provider: "unbound"; apiKey?: string }
1846+
| { provider: "litellm"; apiKey: string; baseUrl: string }

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,11 +391,7 @@ const ApiOptions = ({
391391
)}
392392

393393
{selectedProvider === "litellm" && (
394-
<LiteLLM
395-
apiConfiguration={apiConfiguration}
396-
setApiConfigurationField={setApiConfigurationField}
397-
routerModels={routerModels}
398-
/>
394+
<LiteLLM apiConfiguration={apiConfiguration} setApiConfigurationField={setApiConfigurationField} />
399395
)}
400396

401397
{selectedProvider === "human-relay" && (

0 commit comments

Comments
 (0)