Skip to content

Commit d7787a2

Browse files
feat: add gemini-embedding-001 model to code-index service (#5698)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
1 parent f71275e commit d7787a2

File tree

5 files changed

+149
-35
lines changed

5 files changed

+149
-35
lines changed

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

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ describe("CodeIndexServiceFactory", () => {
265265
expect(() => factory.createEmbedder()).toThrow("serviceFactory.openAiCompatibleConfigMissing")
266266
})
267267

268-
it("should create GeminiEmbedder when using Gemini provider", () => {
268+
it("should create GeminiEmbedder with default model when no modelId specified", () => {
269269
// Arrange
270270
const testConfig = {
271271
embedderProvider: "gemini",
@@ -279,7 +279,25 @@ describe("CodeIndexServiceFactory", () => {
279279
factory.createEmbedder()
280280

281281
// Assert
282-
expect(MockedGeminiEmbedder).toHaveBeenCalledWith("test-gemini-api-key")
282+
expect(MockedGeminiEmbedder).toHaveBeenCalledWith("test-gemini-api-key", undefined)
283+
})
284+
285+
it("should create GeminiEmbedder with specified modelId", () => {
286+
// Arrange
287+
const testConfig = {
288+
embedderProvider: "gemini",
289+
modelId: "text-embedding-004",
290+
geminiOptions: {
291+
apiKey: "test-gemini-api-key",
292+
},
293+
}
294+
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
295+
296+
// Act
297+
factory.createEmbedder()
298+
299+
// Assert
300+
expect(MockedGeminiEmbedder).toHaveBeenCalledWith("test-gemini-api-key", "text-embedding-004")
283301
})
284302

285303
it("should throw error when Gemini API key is missing", () => {
@@ -507,26 +525,51 @@ describe("CodeIndexServiceFactory", () => {
507525
)
508526
})
509527

510-
it("should use fixed dimension 768 for Gemini provider", () => {
528+
it("should use model-specific dimension for Gemini provider", () => {
511529
// Arrange
512530
const testConfig = {
513531
embedderProvider: "gemini",
514-
modelId: "text-embedding-004", // This is ignored by Gemini
532+
modelId: "gemini-embedding-001",
515533
qdrantUrl: "http://localhost:6333",
516534
qdrantApiKey: "test-key",
517535
}
518536
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
537+
mockGetModelDimension.mockReturnValue(3072)
519538

520539
// Act
521540
factory.createVectorStore()
522541

523542
// Assert
524-
// getModelDimension should not be called for Gemini
525-
expect(mockGetModelDimension).not.toHaveBeenCalled()
543+
expect(mockGetModelDimension).toHaveBeenCalledWith("gemini", "gemini-embedding-001")
526544
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
527545
"/test/workspace",
528546
"http://localhost:6333",
529-
768, // Fixed dimension for Gemini
547+
3072,
548+
"test-key",
549+
)
550+
})
551+
552+
it("should use default model dimension for Gemini when modelId not specified", () => {
553+
// Arrange
554+
const testConfig = {
555+
embedderProvider: "gemini",
556+
qdrantUrl: "http://localhost:6333",
557+
qdrantApiKey: "test-key",
558+
}
559+
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
560+
mockGetDefaultModelId.mockReturnValue("gemini-embedding-001")
561+
mockGetModelDimension.mockReturnValue(3072)
562+
563+
// Act
564+
factory.createVectorStore()
565+
566+
// Assert
567+
expect(mockGetDefaultModelId).toHaveBeenCalledWith("gemini")
568+
expect(mockGetModelDimension).toHaveBeenCalledWith("gemini", "gemini-embedding-001")
569+
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
570+
"/test/workspace",
571+
"http://localhost:6333",
572+
3072,
530573
"test-key",
531574
)
532575
})

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

Lines changed: 79 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,30 @@ describe("GeminiEmbedder", () => {
2525
})
2626

2727
describe("constructor", () => {
28-
it("should create an instance with correct fixed values passed to OpenAICompatibleEmbedder", () => {
28+
it("should create an instance with default model when no model specified", () => {
2929
// Arrange
3030
const apiKey = "test-gemini-api-key"
3131

3232
// Act
3333
embedder = new GeminiEmbedder(apiKey)
3434

35+
// Assert
36+
expect(MockedOpenAICompatibleEmbedder).toHaveBeenCalledWith(
37+
"https://generativelanguage.googleapis.com/v1beta/openai/",
38+
apiKey,
39+
"gemini-embedding-001",
40+
2048,
41+
)
42+
})
43+
44+
it("should create an instance with specified model", () => {
45+
// Arrange
46+
const apiKey = "test-gemini-api-key"
47+
const modelId = "text-embedding-004"
48+
49+
// Act
50+
embedder = new GeminiEmbedder(apiKey, modelId)
51+
3552
// Assert
3653
expect(MockedOpenAICompatibleEmbedder).toHaveBeenCalledWith(
3754
"https://generativelanguage.googleapis.com/v1beta/openai/",
@@ -50,7 +67,7 @@ describe("GeminiEmbedder", () => {
5067
})
5168

5269
describe("embedderInfo", () => {
53-
it("should return correct embedder info with dimension 768", () => {
70+
it("should return correct embedder info", () => {
5471
// Arrange
5572
embedder = new GeminiEmbedder("test-api-key")
5673

@@ -61,7 +78,66 @@ describe("GeminiEmbedder", () => {
6178
expect(info).toEqual({
6279
name: "gemini",
6380
})
64-
expect(GeminiEmbedder.dimension).toBe(768)
81+
})
82+
83+
describe("createEmbeddings", () => {
84+
let mockCreateEmbeddings: any
85+
86+
beforeEach(() => {
87+
mockCreateEmbeddings = vitest.fn()
88+
MockedOpenAICompatibleEmbedder.prototype.createEmbeddings = mockCreateEmbeddings
89+
})
90+
91+
it("should use instance model when no model parameter provided", async () => {
92+
// Arrange
93+
embedder = new GeminiEmbedder("test-api-key")
94+
const texts = ["test text 1", "test text 2"]
95+
const mockResponse = {
96+
embeddings: [
97+
[0.1, 0.2],
98+
[0.3, 0.4],
99+
],
100+
}
101+
mockCreateEmbeddings.mockResolvedValue(mockResponse)
102+
103+
// Act
104+
const result = await embedder.createEmbeddings(texts)
105+
106+
// Assert
107+
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "gemini-embedding-001")
108+
expect(result).toEqual(mockResponse)
109+
})
110+
111+
it("should use provided model parameter when specified", async () => {
112+
// Arrange
113+
embedder = new GeminiEmbedder("test-api-key", "text-embedding-004")
114+
const texts = ["test text 1", "test text 2"]
115+
const mockResponse = {
116+
embeddings: [
117+
[0.1, 0.2],
118+
[0.3, 0.4],
119+
],
120+
}
121+
mockCreateEmbeddings.mockResolvedValue(mockResponse)
122+
123+
// Act
124+
const result = await embedder.createEmbeddings(texts, "gemini-embedding-001")
125+
126+
// Assert
127+
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "gemini-embedding-001")
128+
expect(result).toEqual(mockResponse)
129+
})
130+
131+
it("should handle errors from OpenAICompatibleEmbedder", async () => {
132+
// Arrange
133+
embedder = new GeminiEmbedder("test-api-key")
134+
const texts = ["test text"]
135+
const error = new Error("Embedding failed")
136+
mockCreateEmbeddings.mockRejectedValue(error)
137+
138+
// Act & Assert
139+
await expect(embedder.createEmbeddings(texts)).rejects.toThrow("Embedding failed")
140+
})
65141
})
66142
})
67143

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

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,47 +7,51 @@ import { TelemetryService } from "@roo-code/telemetry"
77

88
/**
99
* Gemini embedder implementation that wraps the OpenAI Compatible embedder
10-
* with fixed configuration for Google's Gemini embedding API.
10+
* with configuration for Google's Gemini embedding API.
1111
*
12-
* Fixed values:
13-
* - Base URL: https://generativelanguage.googleapis.com/v1beta/openai/
14-
* - Model: text-embedding-004
15-
* - Dimension: 768
12+
* Supported models:
13+
* - text-embedding-004 (dimension: 768)
14+
* - gemini-embedding-001 (dimension: 2048)
1615
*/
1716
export class GeminiEmbedder implements IEmbedder {
1817
private readonly openAICompatibleEmbedder: OpenAICompatibleEmbedder
1918
private static readonly GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/"
20-
private static readonly GEMINI_MODEL = "text-embedding-004"
21-
private static readonly GEMINI_DIMENSION = 768
19+
private static readonly DEFAULT_MODEL = "gemini-embedding-001"
20+
private readonly modelId: string
2221

2322
/**
2423
* Creates a new Gemini embedder
2524
* @param apiKey The Gemini API key for authentication
25+
* @param modelId The model ID to use (defaults to gemini-embedding-001)
2626
*/
27-
constructor(apiKey: string) {
27+
constructor(apiKey: string, modelId?: string) {
2828
if (!apiKey) {
2929
throw new Error(t("embeddings:validation.apiKeyRequired"))
3030
}
3131

32-
// Create an OpenAI Compatible embedder with Gemini's fixed configuration
32+
// Use provided model or default
33+
this.modelId = modelId || GeminiEmbedder.DEFAULT_MODEL
34+
35+
// Create an OpenAI Compatible embedder with Gemini's configuration
3336
this.openAICompatibleEmbedder = new OpenAICompatibleEmbedder(
3437
GeminiEmbedder.GEMINI_BASE_URL,
3538
apiKey,
36-
GeminiEmbedder.GEMINI_MODEL,
39+
this.modelId,
3740
GEMINI_MAX_ITEM_TOKENS,
3841
)
3942
}
4043

4144
/**
4245
* Creates embeddings for the given texts using Gemini's embedding API
4346
* @param texts Array of text strings to embed
44-
* @param model Optional model identifier (ignored - always uses text-embedding-004)
47+
* @param model Optional model identifier (uses constructor model if not provided)
4548
* @returns Promise resolving to embedding response
4649
*/
4750
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
4851
try {
49-
// Always use the fixed Gemini model, ignoring any passed model parameter
50-
return await this.openAICompatibleEmbedder.createEmbeddings(texts, GeminiEmbedder.GEMINI_MODEL)
52+
// Use the provided model or fall back to the instance's model
53+
const modelToUse = model || this.modelId
54+
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse)
5155
} catch (error) {
5256
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
5357
error: error instanceof Error ? error.message : String(error),
@@ -85,11 +89,4 @@ export class GeminiEmbedder implements IEmbedder {
8589
name: "gemini",
8690
}
8791
}
88-
89-
/**
90-
* Gets the fixed dimension for Gemini embeddings
91-
*/
92-
static get dimension(): number {
93-
return GeminiEmbedder.GEMINI_DIMENSION
94-
}
9592
}

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export class CodeIndexServiceFactory {
6363
if (!config.geminiOptions?.apiKey) {
6464
throw new Error(t("embeddings:serviceFactory.geminiConfigMissing"))
6565
}
66-
return new GeminiEmbedder(config.geminiOptions.apiKey)
66+
return new GeminiEmbedder(config.geminiOptions.apiKey, config.modelId)
6767
}
6868

6969
throw new Error(
@@ -111,9 +111,6 @@ export class CodeIndexServiceFactory {
111111
// First check if a manual dimension is provided (works for all providers)
112112
if (config.modelDimension && config.modelDimension > 0) {
113113
vectorSize = config.modelDimension
114-
} else if (provider === "gemini") {
115-
// Gemini's text-embedding-004 has a fixed dimension of 768
116-
vectorSize = 768
117114
} else {
118115
// Fall back to model-specific dimension from profiles
119116
vectorSize = getModelDimension(provider, modelId)

src/shared/embeddingModels.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const EMBEDDING_MODEL_PROFILES: EmbeddingModelProfiles = {
4848
},
4949
gemini: {
5050
"text-embedding-004": { dimension: 768 },
51+
"gemini-embedding-001": { dimension: 3072, scoreThreshold: 0.4 },
5152
},
5253
}
5354

@@ -134,7 +135,7 @@ export function getDefaultModelId(provider: EmbedderProvider): string {
134135
}
135136

136137
case "gemini":
137-
return "text-embedding-004"
138+
return "gemini-embedding-001"
138139

139140
default:
140141
// Fallback for unknown providers

0 commit comments

Comments
 (0)