Skip to content

Commit c5880ee

Browse files
committed
feat(indexing): runtime embedding dimension detection fallback
1 parent b554850 commit c5880ee

3 files changed

Lines changed: 108 additions & 14 deletions

File tree

packages/kilo-indexing/src/indexing/manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -549,7 +549,7 @@ export class CodeIndexManager {
549549
const ignoreInstance = await loadIgnore(this.workspacePath)
550550
const config = this._configManager!.getConfig()
551551
const baseline = prepared ?? (await this.createBaseline(factory))
552-
const { embedder, vectorStore, scanner, fileWatcher } = factory.createServices(this._cacheManager!, ignoreInstance)
552+
const { embedder, vectorStore, scanner, fileWatcher } = await factory.createServices(this._cacheManager!, ignoreInstance)
553553
fileWatcher.setOverlay?.(baseline?.overlay)
554554
log.info("created indexing services", {
555555
workspacePath: this.workspacePath,

packages/kilo-indexing/src/indexing/service-factory.ts

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { Ignore } from "ignore"
22
import path from "path"
33

4-
import { getDefaultModelId } from "./model-registry"
4+
import { getDefaultModelId, getModelDimension } from "./model-registry"
55
import { resolveEmbeddingProfile } from "./embedding-profile"
66

77
import { OpenAiEmbedder } from "./embedders/openai"
@@ -61,9 +61,10 @@ export class CodeIndexServiceFactory {
6161
}
6262
}
6363

64-
public createEmbedder(): IEmbedder {
64+
public createEmbedder(dimensionOverride?: number): IEmbedder {
6565
const config = this.configManager.getConfig()
6666
const provider = config.embedderProvider
67+
const dimension = dimensionOverride ?? config.modelDimension
6768

6869
if (provider === "kilo") {
6970
if (!config.kiloOptions?.apiKey) throw new Error("Kilo API key is required for embedding.")
@@ -73,7 +74,7 @@ export class CodeIndexServiceFactory {
7374
baseUrl: config.kiloOptions.baseUrl,
7475
organizationId: config.kiloOptions.organizationId,
7576
modelId: config.modelId,
76-
dimensions: config.modelDimension,
77+
dimensions: dimension,
7778
})
7879
}
7980
if (provider === "openai") {
@@ -82,7 +83,7 @@ export class CodeIndexServiceFactory {
8283
}
8384
if (provider === "ollama") {
8485
if (!config.ollamaOptions?.baseUrl) throw new Error("Ollama base URL is required for embedding.")
85-
return new CodeIndexOllamaEmbedder(config.ollamaOptions.baseUrl, config.modelId, config.modelDimension)
86+
return new CodeIndexOllamaEmbedder(config.ollamaOptions.baseUrl, config.modelId, dimension)
8687
}
8788
if (provider === "openai-compatible") {
8889
if (!config.openAiCompatibleOptions?.baseUrl) throw new Error("OpenAI-compatible base URL is required.")
@@ -116,7 +117,7 @@ export class CodeIndexServiceFactory {
116117
config.modelId,
117118
undefined,
118119
config.openRouterOptions.specificProvider,
119-
config.modelDimension,
120+
dimension,
120121
)
121122
}
122123
if (provider === "voyage") {
@@ -169,9 +170,9 @@ export class CodeIndexServiceFactory {
169170
}
170171
}
171172

172-
public createVectorStore(workspacePath = this.workspacePath): IVectorStore {
173+
public createVectorStore(workspacePath = this.workspacePath, dimensionOverride?: number): IVectorStore {
173174
const config = this.configManager.getConfig()
174-
const profile = resolveEmbeddingProfile(config.embedderProvider, config.modelId, config.modelDimension)
175+
const profile = resolveEmbeddingProfile(config.embedderProvider, config.modelId, dimensionOverride ?? config.modelDimension)
175176

176177
if (!profile || profile.dimension <= 0) {
177178
throw new Error(
@@ -246,38 +247,63 @@ export class CodeIndexServiceFactory {
246247
)
247248
}
248249

249-
public createServices(
250+
public async detectEmbeddingDimension(embedder: IEmbedder): Promise<number> {
251+
log.info("detecting embedding dimension", { provider: embedder.embedderInfo.name })
252+
const response = await embedder.createEmbeddings(["test"])
253+
const dimension = response.embeddings[0]?.length
254+
if (!dimension || dimension <= 0) {
255+
throw new Error(`Failed to detect embedding dimension: invalid response from ${embedder.embedderInfo.name}`)
256+
}
257+
log.info("detected embedding dimension", { provider: embedder.embedderInfo.name, dimension })
258+
return dimension
259+
}
260+
261+
public async createServices(
250262
cacheManager: CacheManager,
251263
ignoreInstance: Ignore,
252-
): {
264+
): Promise<{
253265
embedder: IEmbedder
254266
vectorStore: IVectorStore
255267
parser: ICodeParser
256268
scanner: DirectoryScanner
257269
fileWatcher: IFileWatcher
258-
} {
270+
}> {
259271
if (!this.configManager.isFeatureConfigured) {
260272
throw new Error("Code indexing is not configured. Save your settings to start indexing.")
261273
}
262274

263275
const config = this.configManager.getConfig()
276+
const modelId = config.modelId ?? getDefaultModelId(config.embedderProvider)
277+
const registryDimension = getModelDimension(config.embedderProvider, modelId)
278+
264279
log.info("creating indexing services", {
265280
workspacePath: this.workspacePath,
266281
provider: config.embedderProvider,
267282
vectorStore: config.vectorStoreProvider,
268-
model: config.modelId ?? getDefaultModelId(config.embedderProvider),
283+
model: modelId,
269284
configured: config.isConfigured,
270285
})
271286

272-
const embedder = this.createEmbedder()
273-
const vectorStore = this.createVectorStore()
287+
let effectiveDimension: number | undefined
288+
if (config.modelDimension) {
289+
effectiveDimension = config.modelDimension
290+
} else if (registryDimension) {
291+
effectiveDimension = registryDimension
292+
} else {
293+
const detectionEmbedder = this.createEmbedder()
294+
effectiveDimension = await this.detectEmbeddingDimension(detectionEmbedder)
295+
}
296+
297+
const embedder = this.createEmbedder(effectiveDimension)
298+
const vectorStore = this.createVectorStore(this.workspacePath, effectiveDimension)
274299
const parser = codeParser
275300
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
276301
const fileWatcher = this.createFileWatcher(embedder, vectorStore, cacheManager, ignoreInstance)
277302

278303
log.info("indexing services created", {
279304
workspacePath: this.workspacePath,
280305
provider: embedder.embedderInfo.name,
306+
dimension: effectiveDimension,
281307
})
282308

283309
return { embedder, vectorStore, parser, scanner, fileWatcher }

packages/kilo-indexing/test/kilocode/indexing/service-factory.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import { describe, expect, test, mock, beforeEach } from "bun:test"
22
import path from "path"
3+
import type { Ignore } from "ignore"
34
import { mockEmbeddingsCreate, openAIMockFactory, setOpenAIConstructorHook } from "./embedders/__helpers__/openai-mock"
45

56
mock.module("openai", openAIMockFactory)
67
import { CodeIndexServiceFactory } from "../../../src/indexing/service-factory"
78
import { CodeIndexConfigManager } from "../../../src/indexing/config-manager"
89
import { CacheManager } from "../../../src/indexing/cache-manager"
10+
import type { IEmbedder } from "../../../src/indexing/interfaces"
911

1012
const workspacePath = "/tmp/ws"
1113
const cacheDirectory = "/tmp/cache"
@@ -221,4 +223,70 @@ describe("CodeIndexServiceFactory", () => {
221223
dimensions: 1024,
222224
})
223225
})
226+
227+
test("uses registry default for known model without explicit dimension", async () => {
228+
const factory = createFactory({
229+
embedderProvider: "openai",
230+
modelId: "text-embedding-3-small",
231+
modelDimension: undefined,
232+
})
233+
234+
let embedderCreatedWithDimension: number | undefined
235+
const originalCreateEmbedder = factory.createEmbedder.bind(factory)
236+
factory.createEmbedder = function (override?: number) {
237+
embedderCreatedWithDimension = override
238+
return originalCreateEmbedder(override)
239+
}
240+
241+
const services = await factory.createServices({} as CacheManager, {} as Ignore)
242+
243+
expect(embedderCreatedWithDimension).toBe(1536)
244+
expect((services.vectorStore as unknown as { vectorSize: number }).vectorSize).toBe(1536)
245+
})
246+
247+
test("uses explicit dimension over registry default", async () => {
248+
const factory = createFactory({
249+
embedderProvider: "openai",
250+
modelId: "text-embedding-3-small",
251+
modelDimension: 12346789,
252+
})
253+
254+
let embedderCreatedWithDimension: number | undefined
255+
const originalCreateEmbedder = factory.createEmbedder.bind(factory)
256+
factory.createEmbedder = function (override?: number) {
257+
embedderCreatedWithDimension = override
258+
return originalCreateEmbedder(override)
259+
}
260+
261+
const services = await factory.createServices({} as CacheManager, {} as Ignore)
262+
263+
expect(embedderCreatedWithDimension).toBe(12346789)
264+
expect((services.vectorStore as unknown as { vectorSize: number }).vectorSize).toBe(12346789)
265+
})
266+
267+
test("detects dimension for unknown model without explicit dimension", async () => {
268+
const factory = createFactory({
269+
embedderProvider: "openai-compatible",
270+
modelId: "custom/unknown-model",
271+
modelDimension: undefined,
272+
openAiCompatibleBaseUrl: "http://localhost:1234/v1",
273+
})
274+
275+
let detectionCalled = false
276+
const originalDetect = factory.detectEmbeddingDimension.bind(factory)
277+
;(factory as any).detectEmbeddingDimension = async function (embedder: IEmbedder) {
278+
detectionCalled = true
279+
return originalDetect(embedder)
280+
}
281+
282+
mockEmbeddingsCreate.mockResolvedValue({
283+
data: [{ embedding: [0.1, 0.2, 0.3, 0.4, 0.5] }],
284+
usage: { prompt_tokens: 1, total_tokens: 1 },
285+
})
286+
287+
const services = await factory.createServices({} as CacheManager, {} as Ignore)
288+
289+
expect(detectionCalled).toBe(true)
290+
expect((services.vectorStore as unknown as { vectorSize: number }).vectorSize).toBe(5)
291+
})
224292
})

0 commit comments

Comments
 (0)