Skip to content

Commit 83ccedf

Browse files
committed
fix: include API key in Ollama /api/tags requests
- Updated GetModelsOptions type to include optional apiKey for ollama provider - Modified getOllamaModels function to accept and use apiKey parameter in Authorization headers - Updated all callers (webviewMessageHandler, modelCache, native-ollama) to pass apiKey - Added test coverage for API key authentication Fixes #7902
1 parent 8fee312 commit 83ccedf

File tree

6 files changed

+96
-14
lines changed

6 files changed

+96
-14
lines changed

src/api/providers/fetchers/__tests__/ollama.test.ts

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ describe("Ollama Fetcher", () => {
108108
const result = await getOllamaModels(baseUrl)
109109

110110
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
111-
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
111+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
112112

113113
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
114-
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
114+
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName }, { headers: {} })
115115

116116
expect(typeof result).toBe("object")
117117
expect(result).not.toBeInstanceOf(Array)
@@ -130,7 +130,7 @@ describe("Ollama Fetcher", () => {
130130
const result = await getOllamaModels(baseUrl)
131131

132132
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
133-
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
133+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
134134
expect(mockedAxios.post).not.toHaveBeenCalled()
135135
expect(result).toEqual({})
136136
})
@@ -146,7 +146,7 @@ describe("Ollama Fetcher", () => {
146146
const result = await getOllamaModels(baseUrl)
147147

148148
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
149-
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
149+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
150150
expect(mockedAxios.post).not.toHaveBeenCalled()
151151
expect(consoleInfoSpy).toHaveBeenCalledWith(`Failed connecting to Ollama at ${baseUrl}`)
152152
expect(result).toEqual({})
@@ -204,10 +204,10 @@ describe("Ollama Fetcher", () => {
204204
const result = await getOllamaModels(baseUrl)
205205

206206
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
207-
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`)
207+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: {} })
208208

209209
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
210-
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName })
210+
expect(mockedAxios.post).toHaveBeenCalledWith(`${baseUrl}/api/show`, { model: modelName }, { headers: {} })
211211

212212
expect(typeof result).toBe("object")
213213
expect(result).not.toBeInstanceOf(Array)
@@ -217,5 +217,73 @@ describe("Ollama Fetcher", () => {
217217
// Verify the model was parsed correctly despite null families
218218
expect(result[modelName].description).toBe("Family: llama, Context: 4096, Size: 23.6B")
219219
})
220+
221+
it("should include Authorization header when API key is provided", async () => {
222+
const baseUrl = "http://localhost:11434"
223+
const apiKey = "test-api-key-123"
224+
const modelName = "test-model:latest"
225+
226+
const mockApiTagsResponse = {
227+
models: [
228+
{
229+
name: modelName,
230+
model: modelName,
231+
modified_at: "2025-06-03T09:23:22.610222878-04:00",
232+
size: 14333928010,
233+
digest: "6a5f0c01d2c96c687d79e32fdd25b87087feb376bf9838f854d10be8cf3c10a5",
234+
details: {
235+
family: "llama",
236+
families: ["llama"],
237+
format: "gguf",
238+
parameter_size: "23.6B",
239+
parent_model: "",
240+
quantization_level: "Q4_K_M",
241+
},
242+
},
243+
],
244+
}
245+
const mockApiShowResponse = {
246+
license: "Mock License",
247+
modelfile: "FROM /path/to/blob\nTEMPLATE {{ .Prompt }}",
248+
parameters: "num_ctx 4096\nstop_token <eos>",
249+
template: "{{ .System }}USER: {{ .Prompt }}ASSISTANT:",
250+
modified_at: "2025-06-03T09:23:22.610222878-04:00",
251+
details: {
252+
parent_model: "",
253+
format: "gguf",
254+
family: "llama",
255+
families: ["llama"],
256+
parameter_size: "23.6B",
257+
quantization_level: "Q4_K_M",
258+
},
259+
model_info: {
260+
"ollama.context_length": 4096,
261+
"some.other.info": "value",
262+
},
263+
capabilities: ["completion"],
264+
}
265+
266+
mockedAxios.get.mockResolvedValueOnce({ data: mockApiTagsResponse })
267+
mockedAxios.post.mockResolvedValueOnce({ data: mockApiShowResponse })
268+
269+
const result = await getOllamaModels(baseUrl, apiKey)
270+
271+
const expectedHeaders = { Authorization: `Bearer ${apiKey}` }
272+
273+
expect(mockedAxios.get).toHaveBeenCalledTimes(1)
274+
expect(mockedAxios.get).toHaveBeenCalledWith(`${baseUrl}/api/tags`, { headers: expectedHeaders })
275+
276+
expect(mockedAxios.post).toHaveBeenCalledTimes(1)
277+
expect(mockedAxios.post).toHaveBeenCalledWith(
278+
`${baseUrl}/api/show`,
279+
{ model: modelName },
280+
{ headers: expectedHeaders },
281+
)
282+
283+
expect(typeof result).toBe("object")
284+
expect(result).not.toBeInstanceOf(Array)
285+
expect(Object.keys(result).length).toBe(1)
286+
expect(result[modelName]).toBeDefined()
287+
})
220288
})
221289
})

src/api/providers/fetchers/modelCache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
7575
models = await getLiteLLMModels(options.apiKey, options.baseUrl)
7676
break
7777
case "ollama":
78-
models = await getOllamaModels(options.baseUrl)
78+
models = await getOllamaModels(options.baseUrl, options.apiKey)
7979
break
8080
case "lmstudio":
8181
models = await getLMStudioModels(options.baseUrl)

src/api/providers/fetchers/ollama.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,10 @@ export const parseOllamaModel = (rawModel: OllamaModelInfoResponse): ModelInfo =
5454
return modelInfo
5555
}
5656

57-
export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promise<Record<string, ModelInfo>> {
57+
export async function getOllamaModels(
58+
baseUrl = "http://localhost:11434",
59+
apiKey?: string,
60+
): Promise<Record<string, ModelInfo>> {
5861
const models: Record<string, ModelInfo> = {}
5962

6063
// clearing the input can leave an empty string; use the default in that case
@@ -65,17 +68,27 @@ export async function getOllamaModels(baseUrl = "http://localhost:11434"): Promi
6568
return models
6669
}
6770

68-
const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`)
71+
// Prepare headers with optional API key
72+
const headers: Record<string, string> = {}
73+
if (apiKey) {
74+
headers["Authorization"] = `Bearer ${apiKey}`
75+
}
76+
77+
const response = await axios.get<OllamaModelsResponse>(`${baseUrl}/api/tags`, { headers })
6978
const parsedResponse = OllamaModelsResponseSchema.safeParse(response.data)
7079
let modelInfoPromises = []
7180

7281
if (parsedResponse.success) {
7382
for (const ollamaModel of parsedResponse.data.models) {
7483
modelInfoPromises.push(
7584
axios
76-
.post<OllamaModelInfoResponse>(`${baseUrl}/api/show`, {
77-
model: ollamaModel.model,
78-
})
85+
.post<OllamaModelInfoResponse>(
86+
`${baseUrl}/api/show`,
87+
{
88+
model: ollamaModel.model,
89+
},
90+
{ headers },
91+
)
7992
.then((ollamaModelInfo) => {
8093
models[ollamaModel.name] = parseOllamaModel(ollamaModelInfo.data)
8194
}),

src/api/providers/native-ollama.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export class NativeOllamaHandler extends BaseProvider implements SingleCompletio
256256
}
257257

258258
async fetchModel() {
259-
this.models = await getOllamaModels(this.options.ollamaBaseUrl)
259+
this.models = await getOllamaModels(this.options.ollamaBaseUrl, this.options.ollamaApiKey)
260260
return this.getModel()
261261
}
262262

src/core/webview/webviewMessageHandler.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,7 @@ export const webviewMessageHandler = async (
887887
const ollamaModels = await getModels({
888888
provider: "ollama",
889889
baseUrl: ollamaApiConfig.ollamaBaseUrl,
890+
apiKey: ollamaApiConfig.ollamaApiKey,
890891
})
891892

892893
if (Object.keys(ollamaModels).length > 0) {

src/shared/api.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export type GetModelsOptions =
150150
| { provider: "requesty"; apiKey?: string; baseUrl?: string }
151151
| { provider: "unbound"; apiKey?: string }
152152
| { provider: "litellm"; apiKey: string; baseUrl: string }
153-
| { provider: "ollama"; baseUrl?: string }
153+
| { provider: "ollama"; baseUrl?: string; apiKey?: string }
154154
| { provider: "lmstudio"; baseUrl?: string }
155155
| { provider: "deepinfra"; apiKey?: string; baseUrl?: string }
156156
| { provider: "io-intelligence"; apiKey: string }

0 commit comments

Comments
 (0)