Skip to content

Commit c587a27

Browse files
committed
fix: resolve Ollama codebase indexing freeze by correcting API usage
- Fix Ollama embedder to use correct API format: "prompt" instead of "input" - Handle response structure correctly: "embedding" instead of "embeddings" - Implement sequential processing for batch embeddings since Ollama API processes one text at a time - Update validation test to use correct API format - Add comprehensive tests for createEmbeddings method Fixes #5823
1 parent fb374b3 commit c587a27

File tree

2 files changed

+138
-40
lines changed

2 files changed

+138
-40
lines changed

src/services/code-index/embedders/__tests__/ollama.spec.ts

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ describe("CodeIndexOllamaEmbedder", () => {
103103
status: 200,
104104
json: () =>
105105
Promise.resolve({
106-
embeddings: [[0.1, 0.2, 0.3]],
106+
embedding: [0.1, 0.2, 0.3],
107107
}),
108108
} as Response),
109109
)
@@ -126,7 +126,7 @@ describe("CodeIndexOllamaEmbedder", () => {
126126
expect(secondCall[0]).toBe("http://localhost:11434/api/embed")
127127
expect(secondCall[1]?.method).toBe("POST")
128128
expect(secondCall[1]?.headers).toEqual({ "Content-Type": "application/json" })
129-
expect(secondCall[1]?.body).toBe(JSON.stringify({ model: "nomic-embed-text", input: ["test"] }))
129+
expect(secondCall[1]?.body).toBe(JSON.stringify({ model: "nomic-embed-text", prompt: "test" }))
130130
expect(secondCall[1]?.signal).toBeDefined() // AbortSignal for timeout
131131
})
132132

@@ -240,4 +240,94 @@ describe("CodeIndexOllamaEmbedder", () => {
240240
expect(result.error).toBe("Network timeout")
241241
})
242242
})
243+
244+
describe("createEmbeddings", () => {
245+
it("should create embeddings for multiple texts using sequential calls", async () => {
246+
// Mock successful responses for each individual text
247+
mockFetch
248+
.mockImplementationOnce(() =>
249+
Promise.resolve({
250+
ok: true,
251+
status: 200,
252+
json: () =>
253+
Promise.resolve({
254+
embedding: [0.1, 0.2, 0.3],
255+
}),
256+
} as Response),
257+
)
258+
.mockImplementationOnce(() =>
259+
Promise.resolve({
260+
ok: true,
261+
status: 200,
262+
json: () =>
263+
Promise.resolve({
264+
embedding: [0.4, 0.5, 0.6],
265+
}),
266+
} as Response),
267+
)
268+
269+
const result = await embedder.createEmbeddings(["text1", "text2"])
270+
271+
expect(result.embeddings).toEqual([
272+
[0.1, 0.2, 0.3],
273+
[0.4, 0.5, 0.6],
274+
])
275+
expect(mockFetch).toHaveBeenCalledTimes(2)
276+
277+
// Check first call
278+
const firstCall = mockFetch.mock.calls[0]
279+
expect(firstCall[0]).toBe("http://localhost:11434/api/embed")
280+
expect(firstCall[1]?.method).toBe("POST")
281+
expect(firstCall[1]?.headers).toEqual({ "Content-Type": "application/json" })
282+
expect(firstCall[1]?.body).toBe(JSON.stringify({ model: "nomic-embed-text", prompt: "text1" }))
283+
expect(firstCall[1]?.signal).toBeDefined()
284+
285+
// Check second call
286+
const secondCall = mockFetch.mock.calls[1]
287+
expect(secondCall[0]).toBe("http://localhost:11434/api/embed")
288+
expect(secondCall[1]?.method).toBe("POST")
289+
expect(secondCall[1]?.headers).toEqual({ "Content-Type": "application/json" })
290+
expect(secondCall[1]?.body).toBe(JSON.stringify({ model: "nomic-embed-text", prompt: "text2" }))
291+
expect(secondCall[1]?.signal).toBeDefined()
292+
})
293+
294+
it("should handle errors during embedding creation", async () => {
295+
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))
296+
297+
await expect(embedder.createEmbeddings(["test"])).rejects.toThrow(
298+
"embeddings:ollama.serviceNotRunning",
299+
)
300+
})
301+
302+
it("should handle invalid response structure", async () => {
303+
mockFetch.mockImplementationOnce(() =>
304+
Promise.resolve({
305+
ok: true,
306+
status: 200,
307+
json: () =>
308+
Promise.resolve({
309+
// Missing 'embedding' field
310+
invalid: "response",
311+
}),
312+
} as Response),
313+
)
314+
315+
await expect(embedder.createEmbeddings(["test"])).rejects.toThrow(
316+
"embeddings:ollama.invalidResponseStructure",
317+
)
318+
})
319+
320+
it("should handle HTTP error responses", async () => {
321+
mockFetch.mockImplementationOnce(() =>
322+
Promise.resolve({
323+
ok: false,
324+
status: 500,
325+
statusText: "Internal Server Error",
326+
text: () => Promise.resolve("Server error details"),
327+
} as Response),
328+
)
329+
330+
await expect(embedder.createEmbeddings(["test"])).rejects.toThrow("embeddings:ollama.requestFailed")
331+
})
332+
})
243333
})

src/services/code-index/embedders/ollama.ts

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
2626

2727
/**
2828
* Creates embeddings for the given texts using the specified Ollama model.
29+
* Ollama's /api/embed endpoint processes one text at a time, so we make sequential calls.
2930
* @param texts - An array of strings to embed.
3031
* @param model - Optional model ID to override the default.
3132
* @returns A promise that resolves to an EmbeddingResponse containing the embeddings and usage data.
3233
*/
3334
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
3435
const modelToUse = model || this.defaultModelId
35-
const url = `${this.baseUrl}/api/embed` // Endpoint as specified
36+
const url = `${this.baseUrl}/api/embed`
3637

3738
// Apply model-specific query prefix if required
3839
const queryPrefix = getModelQueryPrefix("ollama", modelToUse)
@@ -60,48 +61,55 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
6061
: texts
6162

6263
try {
63-
// Note: Standard Ollama API uses 'prompt' for single text, not 'input' for array.
64-
// Implementing based on user's specific request structure.
64+
const embeddings: number[][] = []
6565

66-
// Add timeout to prevent indefinite hanging
67-
const controller = new AbortController()
68-
const timeoutId = setTimeout(() => controller.abort(), OLLAMA_EMBEDDING_TIMEOUT_MS)
66+
// Process each text individually since Ollama's /api/embed endpoint
67+
// expects a single 'prompt' field, not an array of inputs
68+
for (let i = 0; i < processedTexts.length; i++) {
69+
const text = processedTexts[i]
6970

70-
const response = await fetch(url, {
71-
method: "POST",
72-
headers: {
73-
"Content-Type": "application/json",
74-
},
75-
body: JSON.stringify({
76-
model: modelToUse,
77-
input: processedTexts, // Using 'input' as requested
78-
}),
79-
signal: controller.signal,
80-
})
81-
clearTimeout(timeoutId)
71+
// Add timeout to prevent indefinite hanging
72+
const controller = new AbortController()
73+
const timeoutId = setTimeout(() => controller.abort(), OLLAMA_EMBEDDING_TIMEOUT_MS)
8274

83-
if (!response.ok) {
84-
let errorBody = t("embeddings:ollama.couldNotReadErrorBody")
85-
try {
86-
errorBody = await response.text()
87-
} catch (e) {
88-
// Ignore error reading body
89-
}
90-
throw new Error(
91-
t("embeddings:ollama.requestFailed", {
92-
status: response.status,
93-
statusText: response.statusText,
94-
errorBody,
75+
const response = await fetch(url, {
76+
method: "POST",
77+
headers: {
78+
"Content-Type": "application/json",
79+
},
80+
body: JSON.stringify({
81+
model: modelToUse,
82+
prompt: text, // Ollama expects 'prompt', not 'input'
9583
}),
96-
)
97-
}
84+
signal: controller.signal,
85+
})
86+
clearTimeout(timeoutId)
87+
88+
if (!response.ok) {
89+
let errorBody = t("embeddings:ollama.couldNotReadErrorBody")
90+
try {
91+
errorBody = await response.text()
92+
} catch (e) {
93+
// Ignore error reading body
94+
}
95+
throw new Error(
96+
t("embeddings:ollama.requestFailed", {
97+
status: response.status,
98+
statusText: response.statusText,
99+
errorBody,
100+
}),
101+
)
102+
}
98103

99-
const data = await response.json()
104+
const data = await response.json()
105+
106+
// Ollama returns 'embedding' (singular), not 'embeddings' (plural)
107+
const embedding = data.embedding
108+
if (!embedding || !Array.isArray(embedding)) {
109+
throw new Error(t("embeddings:ollama.invalidResponseStructure"))
110+
}
100111

101-
// Extract embeddings using 'embeddings' key as requested
102-
const embeddings = data.embeddings
103-
if (!embeddings || !Array.isArray(embeddings)) {
104-
throw new Error(t("embeddings:ollama.invalidResponseStructure"))
112+
embeddings.push(embedding)
105113
}
106114

107115
return {
@@ -210,7 +218,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
210218
},
211219
body: JSON.stringify({
212220
model: this.defaultModelId,
213-
input: ["test"],
221+
prompt: "test", // Use 'prompt' instead of 'input' array
214222
}),
215223
signal: testController.signal,
216224
})

0 commit comments

Comments
 (0)