fix: make LiteLLM API key optional for self-hosted proxies

LiteLLM supports and is often used without authentication in self-hosted
or local environments. This change makes the API key field optional so
users can configure LiteLLM with just a base URL.

Changes:
- Remove API key requirement from validation (require only base URL)
- Update Refresh Models button to work without API key
- Make apiKey optional in LiteLLM type definition
- Update fetcher to accept undefined apiKey and skip Auth header
- Update webviewMessageHandler to only require baseUrl
- Add validation.baseUrl translation key
- Add tests for no-API-key scenarios

Closes #11918
This commit is contained in:
Roo Code 2026-03-13 10:21:07 +00:00
parent 44fd975b17
commit bc55c0068a
8 changed files with 54 additions and 12 deletions

View file

@ -697,4 +697,44 @@ describe("getLiteLLMModels", () => {
description: "model-with-only-max-output-tokens via LiteLLM proxy",
})
})
it("sends request without Authorization header when apiKey is undefined", async () => {
const mockResponse = {
data: {
data: [],
},
}
mockedAxios.get.mockResolvedValue(mockResponse)
await getLiteLLMModels(undefined, "http://localhost:4000")
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", {
headers: {
"Content-Type": "application/json",
...DEFAULT_HEADERS,
},
timeout: 5000,
})
})
it("sends request without Authorization header when apiKey is empty string", async () => {
const mockResponse = {
data: {
data: [],
},
}
mockedAxios.get.mockResolvedValue(mockResponse)
await getLiteLLMModels("", "http://localhost:4000")
expect(mockedAxios.get).toHaveBeenCalledWith("http://localhost:4000/v1/model/info", {
headers: {
"Content-Type": "application/json",
...DEFAULT_HEADERS,
},
timeout: 5000,
})
})
})

View file

@ -11,7 +11,7 @@ import { DEFAULT_HEADERS } from "../constants"
* @returns A promise that resolves to a record of model IDs to model info
* @throws Will throw an error if the request fails or the response is not as expected.
*/
export async function getLiteLLMModels(apiKey: string, baseUrl: string): Promise<ModelRecord> {
export async function getLiteLLMModels(apiKey: string | undefined, baseUrl: string): Promise<ModelRecord> {
try {
const headers: Record<string, string> = {
"Content-Type": "application/json",

View file

@ -26,7 +26,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa
options,
name: "litellm",
baseURL: `${options.litellmBaseUrl || "http://localhost:4000"}`,
apiKey: options.litellmApiKey || "dummy-key",
apiKey: options.litellmApiKey || "not-needed",
modelId: options.litellmModelId,
defaultModelId: litellmDefaultModelId,
defaultModelInfo: litellmDefaultModelInfo,

View file

@ -1001,11 +1001,11 @@ export const webviewMessageHandler = async (
},
]
// LiteLLM is conditional on baseUrl+apiKey
// LiteLLM is conditional on baseUrl (apiKey is optional)
const litellmApiKey = apiConfiguration.litellmApiKey || message?.values?.litellmApiKey
const litellmBaseUrl = apiConfiguration.litellmBaseUrl || message?.values?.litellmBaseUrl
if (litellmApiKey && litellmBaseUrl) {
if (litellmBaseUrl) {
// If explicit credentials are provided in message.values (from Refresh Models button),
// flush the cache first to ensure we fetch fresh data with the new credentials
if (message?.values?.litellmApiKey || message?.values?.litellmBaseUrl) {

View file

@ -171,7 +171,7 @@ type CommonFetchParams = {
const dynamicProviderExtras = {
openrouter: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
"vercel-ai-gateway": {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type
litellm: {} as { apiKey: string; baseUrl: string },
litellm: {} as { apiKey?: string; baseUrl: string },
requesty: {} as { apiKey?: string; baseUrl?: string },
unbound: {} as { apiKey?: string },
ollama: {} as {}, // eslint-disable-line @typescript-eslint/no-empty-object-type

View file

@ -86,13 +86,16 @@ export const LiteLLM = ({
const key = apiConfiguration.litellmApiKey
const url = apiConfiguration.litellmBaseUrl
if (!key || !url) {
if (!url) {
setRefreshStatus("error")
setRefreshError(t("settings:providers.refreshModels.missingConfig"))
return
}
vscode.postMessage({ type: "requestRouterModels", values: { litellmApiKey: key, litellmBaseUrl: url } })
vscode.postMessage({
type: "requestRouterModels",
values: { ...(key ? { litellmApiKey: key } : {}), litellmBaseUrl: url },
})
}, [apiConfiguration, setRefreshStatus, setRefreshError, t])
return (
@ -121,9 +124,7 @@ export const LiteLLM = ({
<Button
variant="outline"
onClick={handleRefreshModels}
disabled={
refreshStatus === "loading" || !apiConfiguration.litellmApiKey || !apiConfiguration.litellmBaseUrl
}
disabled={refreshStatus === "loading" || !apiConfiguration.litellmBaseUrl}
className="w-full">
<div className="flex items-center gap-2">
{refreshStatus === "loading" ? (

View file

@ -944,6 +944,7 @@
},
"validation": {
"apiKey": "You must provide a valid API key.",
"baseUrl": "You must provide a valid base URL.",
"awsRegion": "You must choose a region to use with Amazon Bedrock.",
"googleCloud": "You must provide a valid Google Cloud Project ID and Region.",
"modelId": "You must provide a valid model ID.",

View file

@ -54,8 +54,8 @@ function validateModelsAndKeysProvided(apiConfiguration: ProviderSettings): stri
}
break
case "litellm":
if (!apiConfiguration.litellmApiKey) {
return i18next.t("settings:validation.apiKey")
if (!apiConfiguration.litellmBaseUrl) {
return i18next.t("settings:validation.baseUrl")
}
break
case "anthropic":