Skip to content

Commit 1cdfce0

Browse files
committed
feat: Add geminiEmbeddingDimension to configuration and update related components
1 parent 8d48e72 commit 1cdfce0

File tree

8 files changed

+173
-29
lines changed

8 files changed

+173
-29
lines changed

packages/types/src/codebase-index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export const codebaseIndexConfigSchema = z.object({
1111
codebaseIndexEmbedderBaseUrl: z.string().optional(),
1212
codebaseIndexEmbedderModelId: z.string().optional(),
1313
geminiEmbeddingTaskType: z.string().optional(),
14+
geminiEmbeddingDimension: z.number().optional(),
1415
})
1516

1617
export type CodebaseIndexConfig = z.infer<typeof codebaseIndexConfigSchema>

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,7 @@ const geminiSchema = apiModelIdProviderModelSchema.extend({
152152
geminiApiKey: z.string().optional(),
153153
googleGeminiBaseUrl: z.string().optional(),
154154
geminiEmbeddingTaskType: z.string().optional(),
155+
geminiEmbeddingDimension: z.number().optional(),
155156
})
156157

157158
const openAiNativeSchema = apiModelIdProviderModelSchema.extend({

src/services/code-index/config-manager.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,18 @@ import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config"
55
import { SEARCH_MIN_SCORE } from "./constants"
66
import { getDefaultModelId, getModelDimension } from "../../shared/embeddingModels"
77

8+
// Define a type for the raw config state from globalState
9+
interface RawCodebaseIndexConfigState {
10+
codebaseIndexEnabled?: boolean
11+
codebaseIndexQdrantUrl?: string
12+
codebaseIndexSearchMinScore?: number // Assuming this is also from globalState based on default
13+
codebaseIndexEmbedderProvider?: "openai" | "ollama" | "gemini"
14+
codebaseIndexEmbedderBaseUrl?: string
15+
codebaseIndexEmbedderModelId?: string
16+
geminiEmbeddingTaskType?: string
17+
geminiEmbeddingDimension?: number // Ensure this is part of the raw state type
18+
}
19+
820
/**
921
* Manages configuration state and validation for the code indexing feature.
1022
* Handles loading, validating, and providing access to configuration values.
@@ -31,15 +43,16 @@ export class CodeIndexConfigManager {
3143
*/
3244
private _loadAndSetConfiguration(): void {
3345
// Load configuration from storage
34-
const codebaseIndexConfig = this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
46+
const rawConfig = (this.contextProxy?.getGlobalState("codebaseIndexConfig") ?? {
3547
codebaseIndexEnabled: false,
3648
codebaseIndexQdrantUrl: "http://localhost:6333",
3749
codebaseIndexSearchMinScore: 0.4,
3850
codebaseIndexEmbedderProvider: "openai",
3951
codebaseIndexEmbedderBaseUrl: "",
4052
codebaseIndexEmbedderModelId: "",
4153
geminiEmbeddingTaskType: "CODE_RETRIEVAL_QUERY",
42-
}
54+
geminiEmbeddingDimension: undefined,
55+
}) as RawCodebaseIndexConfigState // Cast to our defined raw state type
4356

4457
const {
4558
codebaseIndexEnabled,
@@ -48,11 +61,13 @@ export class CodeIndexConfigManager {
4861
codebaseIndexEmbedderBaseUrl,
4962
codebaseIndexEmbedderModelId,
5063
geminiEmbeddingTaskType,
51-
} = codebaseIndexConfig
64+
geminiEmbeddingDimension,
65+
} = rawConfig // Destructure from the typed rawConfig
5266

5367
const openAiKey = this.contextProxy?.getSecret("codeIndexOpenAiKey") ?? ""
5468
const qdrantApiKey = this.contextProxy?.getSecret("codeIndexQdrantApiKey") ?? ""
5569
const geminiApiKey = this.contextProxy?.getSecret("geminiApiKey") ?? ""
70+
const rateLimitSeconds = this.contextProxy?.getGlobalState("rateLimitSeconds") ?? undefined
5671

5772
// Update instance variables with configuration
5873
this.isEnabled = codebaseIndexEnabled || false
@@ -79,6 +94,8 @@ export class CodeIndexConfigManager {
7994
geminiApiKey,
8095
geminiEmbeddingTaskType: geminiEmbeddingTaskType || "CODE_RETRIEVAL_QUERY",
8196
apiModelId: this.modelId,
97+
geminiEmbeddingDimension,
98+
rateLimitSeconds,
8299
}
83100
}
84101

@@ -159,6 +176,7 @@ export class CodeIndexConfigManager {
159176
// Gemini requires an API key and Qdrant URL
160177
const geminiApiKey = this.geminiOptions?.geminiApiKey
161178
const geminiEmbeddingTaskType = this.geminiOptions?.geminiEmbeddingTaskType
179+
162180
const qdrantUrl = this.qdrantUrl
163181
const isConfigured = !!(geminiApiKey && geminiEmbeddingTaskType && qdrantUrl)
164182
return isConfigured
@@ -180,6 +198,7 @@ export class CodeIndexConfigManager {
180198
const prevOpenAiKey = prev?.openAiKey ?? ""
181199
const prevOllamaBaseUrl = prev?.ollamaBaseUrl ?? ""
182200
const prevGeminiApiKey = prev?.geminiApiKey ?? ""
201+
const prevGeminiEmbeddingDimension = prev?.geminiEmbeddingDimension // Access from prev
183202
const prevQdrantUrl = prev?.qdrantUrl ?? ""
184203
const prevQdrantApiKey = prev?.qdrantApiKey ?? ""
185204

@@ -205,7 +224,9 @@ export class CodeIndexConfigManager {
205224
return true
206225
}
207226

208-
if (this._hasVectorDimensionChanged(prevProvider, prevModelId)) {
227+
// Check for dimension change, including the new geminiEmbeddingDimension
228+
if (this._hasVectorDimensionChanged(prevProvider, prevModelId, prev.geminiEmbeddingDimension)) {
229+
// Use prev.geminiEmbeddingDimension
209230
return true
210231
}
211232

@@ -229,6 +250,11 @@ export class CodeIndexConfigManager {
229250
if (prevGeminiApiKey !== currentGeminiApiKey) {
230251
return true
231252
}
253+
254+
const currentGeminiEmbeddingDimension = this.geminiOptions?.geminiEmbeddingDimension
255+
if (currentGeminiEmbeddingDimension !== prevGeminiEmbeddingDimension) {
256+
return true
257+
}
232258
}
233259

234260
// Qdrant configuration changes
@@ -246,19 +272,35 @@ export class CodeIndexConfigManager {
246272
/**
247273
* Checks if model changes result in vector dimension changes that require restart.
248274
*/
249-
private _hasVectorDimensionChanged(prevProvider: EmbedderProvider, prevModelId?: string): boolean {
275+
private _hasVectorDimensionChanged(
276+
prevProvider: EmbedderProvider,
277+
prevModelId?: string,
278+
prevGeminiDimension?: number,
279+
): boolean {
250280
const currentProvider = this.embedderProvider
251281
const currentModelId = this.modelId ?? getDefaultModelId(currentProvider)
252282
const resolvedPrevModelId = prevModelId ?? getDefaultModelId(prevProvider)
253283

254284
// If model IDs are the same and provider is the same, no dimension change
255285
if (prevProvider === currentProvider && resolvedPrevModelId === currentModelId) {
286+
// If provider and model are same, check if gemini dimension changed
287+
if (currentProvider === "gemini" && this.geminiOptions?.geminiEmbeddingDimension !== prevGeminiDimension) {
288+
return true
289+
}
256290
return false
257291
}
258292

259293
// Get vector dimensions for both models
260-
const prevDimension = getModelDimension(prevProvider, resolvedPrevModelId)
261-
const currentDimension = getModelDimension(currentProvider, currentModelId)
294+
const prevDimension = getModelDimension(
295+
prevProvider,
296+
resolvedPrevModelId,
297+
prevProvider === "gemini" ? prevGeminiDimension : undefined,
298+
)
299+
const currentDimension = getModelDimension(
300+
currentProvider,
301+
currentModelId,
302+
currentProvider === "gemini" ? this.geminiOptions?.geminiEmbeddingDimension : undefined,
303+
)
262304

263305
// If we can't determine dimensions, be safe and restart
264306
if (prevDimension === undefined || currentDimension === undefined) {
@@ -284,6 +326,7 @@ export class CodeIndexConfigManager {
284326
qdrantUrl: this.qdrantUrl,
285327
qdrantApiKey: this.qdrantApiKey,
286328
searchMinScore: this.searchMinScore,
329+
geminiEmbeddingDimension: this.geminiEmbeddingDimension,
287330
}
288331
}
289332

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

Lines changed: 45 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { GEMINI_RATE_LIMIT_DELAY_MS, MAX_BATCH_RETRIES, INITIAL_RETRY_DELAY_MS }
99
export class CodeIndexGeminiEmbedder extends GeminiHandler implements IEmbedder {
1010
private readonly defaultModelId: string
1111
private readonly defaultTaskType: string
12+
private embeddingQueue: Promise<void> = Promise.resolve() // Sequential queue for embedding operations
1213

1314
/**
1415
* Creates a new Gemini embedder instance.
@@ -21,24 +22,47 @@ export class CodeIndexGeminiEmbedder extends GeminiHandler implements IEmbedder
2122
}
2223

2324
/**
24-
* Creates embeddings for the given texts using the Gemini API.
25+
* Creates embeddings for the given texts using the Gemini API, ensuring sequential processing.
2526
* @param texts - An array of strings to embed.
2627
* @param model - Optional model ID to override the default.
27-
* @returns A promise that resolves to an EmbeddingResponse containing the embeddings and usage data.
28+
* @returns A promise that resolves to an EmbeddingResponse containing the embeddings.
2829
*/
29-
// Removed async keyword from the method signature as it no longer uses await at the top level.
30-
// It constructs and returns a promise.
3130
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
32-
try {
33-
const modelId = model || this.defaultModelId
34-
const result = await this.embedWithTokenLimit(texts, modelId, this.defaultTaskType)
35-
return {
36-
embeddings: result.embeddings,
31+
// This function will be executed when it's this task's turn in the queue.
32+
const taskExecution = async (): Promise<EmbeddingResponse> => {
33+
try {
34+
const modelId = model || this.defaultModelId
35+
// embedWithTokenLimit handles batching, internal delays, and retries for API calls.
36+
const result = await this.embedWithTokenLimit(texts, modelId, this.defaultTaskType)
37+
return {
38+
embeddings: result.embeddings,
39+
// If EmbeddingResponse is updated to include usage, and result.usage is reliable:
40+
// usage: result.usage,
41+
}
42+
} catch (error: any) {
43+
// Errors are logged within embedWithTokenLimit or _embedBatchWithRetries.
44+
// This re-throws the error to be caught by the specific caller of createEmbeddings.
45+
console.error("Error during Gemini embedding task execution in queue:", error.message)
46+
throw error
3747
}
38-
} catch (error: any) {
39-
console.error("Gemini embedding task failed:", error)
40-
throw error
4148
}
49+
50+
// Chain this task onto the queue.
51+
// The actual execution of taskExecution() is deferred until the previous promise in the queue resolves.
52+
const taskPromise = this.embeddingQueue.then(taskExecution)
53+
54+
// Update the queue to wait for the current task to complete (or fail).
55+
// .catch(() => {}) ensures that an error in one task doesn't break the queue for subsequent tasks.
56+
// Each task's success/failure is handled by its own promise (taskPromise), which is returned to the caller.
57+
this.embeddingQueue = taskPromise
58+
.catch(() => {
59+
// This task failed, but the queue should proceed for the next one.
60+
// The error from taskPromise will be handled by its specific awaiter below.
61+
})
62+
.then(() => undefined) // Ensure the queue promise resolves to void for the next .then() in the chain.
63+
64+
// Return the promise for this specific task. The caller will await this.
65+
return taskPromise
4266
}
4367

4468
/**
@@ -112,12 +136,15 @@ export class CodeIndexGeminiEmbedder extends GeminiHandler implements IEmbedder
112136

113137
// Process the current batch if not empty
114138
if (currentBatch.length > 0) {
115-
const delayMs =
116-
this.options.rateLimitSeconds !== undefined
117-
? this.options.rateLimitSeconds * 1000
118-
: GEMINI_RATE_LIMIT_DELAY_MS
119-
console.log(`Adding proactive delay of ${delayMs}ms before Gemini batch`)
120-
await new Promise((resolve) => setTimeout(resolve, delayMs))
139+
if (!isFirstBatch) {
140+
const delayMs =
141+
this.options.rateLimitSeconds !== undefined
142+
? this.options.rateLimitSeconds * 1000
143+
: GEMINI_RATE_LIMIT_DELAY_MS
144+
console.log(`Adding proactive delay of ${delayMs}ms before Gemini batch`)
145+
await new Promise((resolve) => setTimeout(resolve, delayMs))
146+
isFirstBatch = false
147+
}
121148

122149
try {
123150
const batchResult = await this._embedBatchWithRetries(currentBatch, model, taskType)

src/services/code-index/interfaces/config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface CodeIndexConfig {
1515
qdrantUrl?: string
1616
qdrantApiKey?: string
1717
searchMinScore?: number
18+
geminiEmbeddingDimension?: number
1819
}
1920

2021
/**
@@ -29,6 +30,7 @@ export type PreviousConfigSnapshot = {
2930
ollamaBaseUrl?: string
3031
geminiApiKey?: string
3132
geminiEmbeddingTaskType?: string
33+
geminiEmbeddingDimension?: number // Add here
3234
qdrantUrl?: string
3335
qdrantApiKey?: string
3436
}

src/services/code-index/service-factory.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export class CodeIndexServiceFactory {
4444
...config.ollamaOptions,
4545
ollamaModelId: config.modelId,
4646
})
47-
} else if (provider === "gemini") {
47+
} else if (provider === "gemini") {
4848
if (!config.geminiOptions?.geminiApiKey) {
4949
throw new Error("Gemini configuration missing for embedder creation")
5050
}
@@ -64,8 +64,12 @@ export class CodeIndexServiceFactory {
6464
const defaultModel = getDefaultModelId(provider)
6565
// Use the embedding model ID from config, not the chat model IDs
6666
const modelId = config.modelId ?? defaultModel
67+
let requestedDimension: number | undefined
68+
if (provider === "gemini") {
69+
requestedDimension = config.geminiEmbeddingDimension
70+
}
6771

68-
const vectorSize = getModelDimension(provider, modelId)
72+
const vectorSize = getModelDimension(provider, modelId, requestedDimension)
6973

7074
if (vectorSize === undefined) {
7175
throw new Error(

src/shared/embeddingModels.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,14 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = {
4848
* Retrieves the embedding dimension for a given provider and model ID.
4949
* @param provider The embedder provider (e.g., "openai").
5050
* @param modelId The specific model ID (e.g., "text-embedding-3-small").
51+
* @param requestedDimension Optional dimension requested by the user.
5152
* @returns The dimension size or undefined if the model is not found.
5253
*/
53-
export function getModelDimension(provider: EmbedderProvider, modelId: string): number | undefined {
54+
export function getModelDimension(
55+
provider: EmbedderProvider,
56+
modelId: string,
57+
requestedDimension?: number,
58+
): number | undefined {
5459
const providerProfiles = EMBEDDING_MODEL_PROFILES[provider]
5560
if (!providerProfiles) {
5661
console.warn(`Provider not found in profiles: ${provider}`)
@@ -64,6 +69,14 @@ export function getModelDimension(provider: EmbedderProvider, modelId: string):
6469
return undefined // Or potentially return a default/fallback dimension?
6570
}
6671

72+
if (
73+
requestedDimension &&
74+
modelProfile.supportDimensions &&
75+
modelProfile.supportDimensions.includes(requestedDimension)
76+
) {
77+
return requestedDimension
78+
}
79+
6780
return modelProfile.dimension
6881
}
6982

0 commit comments

Comments
 (0)