Skip to content

Commit 22abb2c

Browse files
committed
fix: exclude encoding_format parameter for Google Gemini embeddings API
- Added isGeminiEndpoint detection to OpenAICompatibleEmbedder - Conditionally include encoding_format only for non-Gemini endpoints - Gemini API does not support the OpenAI-specific encoding_format parameter - Added comprehensive tests to verify Gemini compatibility Fixes #7761
1 parent 247da38 commit 22abb2c

File tree

2 files changed

+185
-14
lines changed

2 files changed

+185
-14
lines changed

src/services/code-index/embedders/__tests__/openai-compatible.spec.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1051,4 +1051,139 @@ describe("OpenAICompatibleEmbedder", () => {
10511051
expect(result.error).toBe("embeddings:validation.configurationError")
10521052
})
10531053
})
1054+
1055+
describe("Gemini compatibility", () => {
1056+
let geminiEmbedder: OpenAICompatibleEmbedder
1057+
const geminiBaseUrl = "https://generativelanguage.googleapis.com/v1beta/openai/"
1058+
const geminiApiKey = "test-gemini-api-key"
1059+
const geminiModelId = "gemini-embedding-001"
1060+
1061+
beforeEach(() => {
1062+
vitest.clearAllMocks()
1063+
geminiEmbedder = new OpenAICompatibleEmbedder(geminiBaseUrl, geminiApiKey, geminiModelId)
1064+
})
1065+
1066+
it("should NOT include encoding_format for Gemini endpoints", async () => {
1067+
const testTexts = ["Hello world"]
1068+
const mockResponse = {
1069+
data: [{ embedding: [0.1, 0.2, 0.3] }],
1070+
usage: { prompt_tokens: 10, total_tokens: 15 },
1071+
}
1072+
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
1073+
1074+
await geminiEmbedder.createEmbeddings(testTexts)
1075+
1076+
// Verify that encoding_format is NOT included for Gemini
1077+
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
1078+
input: testTexts,
1079+
model: geminiModelId,
1080+
// encoding_format should NOT be present
1081+
})
1082+
1083+
// Verify the call doesn't have encoding_format property
1084+
const callArgs = mockEmbeddingsCreate.mock.calls[0][0]
1085+
expect(callArgs).not.toHaveProperty("encoding_format")
1086+
})
1087+
1088+
it("should still include encoding_format for non-Gemini OpenAI-compatible endpoints", async () => {
1089+
// Create a non-Gemini embedder
1090+
const regularEmbedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
1091+
1092+
const testTexts = ["Hello world"]
1093+
const mockResponse = {
1094+
data: [{ embedding: [0.1, 0.2, 0.3] }],
1095+
usage: { prompt_tokens: 10, total_tokens: 15 },
1096+
}
1097+
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
1098+
1099+
await regularEmbedder.createEmbeddings(testTexts)
1100+
1101+
// Verify that encoding_format IS included for non-Gemini endpoints
1102+
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
1103+
input: testTexts,
1104+
model: testModelId,
1105+
encoding_format: "base64",
1106+
})
1107+
})
1108+
1109+
it("should correctly identify Gemini URLs", () => {
1110+
const geminiUrls = [
1111+
"https://generativelanguage.googleapis.com/v1beta/openai/",
1112+
"https://generativelanguage.googleapis.com/v1/openai/",
1113+
"https://generativelanguage.googleapis.com/v2/embeddings",
1114+
]
1115+
1116+
geminiUrls.forEach((url) => {
1117+
const embedder = new OpenAICompatibleEmbedder(url, geminiApiKey, geminiModelId)
1118+
const isGemini = (embedder as any).isGeminiUrl(url)
1119+
expect(isGemini).toBe(true)
1120+
})
1121+
})
1122+
1123+
it("should not identify non-Gemini URLs as Gemini", () => {
1124+
const nonGeminiUrls = [
1125+
"https://api.openai.com/v1",
1126+
"https://api.example.com/embeddings",
1127+
"https://myinstance.openai.azure.com/openai/deployments/my-deployment/embeddings",
1128+
"http://localhost:8080",
1129+
]
1130+
1131+
nonGeminiUrls.forEach((url) => {
1132+
const embedder = new OpenAICompatibleEmbedder(url, testApiKey, testModelId)
1133+
const isGemini = (embedder as any).isGeminiUrl(url)
1134+
expect(isGemini).toBe(false)
1135+
})
1136+
})
1137+
1138+
it("should validate Gemini configuration without encoding_format", async () => {
1139+
const mockResponse = {
1140+
data: [{ embedding: [0.1, 0.2, 0.3] }],
1141+
usage: { prompt_tokens: 2, total_tokens: 2 },
1142+
}
1143+
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
1144+
1145+
const result = await geminiEmbedder.validateConfiguration()
1146+
1147+
expect(result.valid).toBe(true)
1148+
expect(result.error).toBeUndefined()
1149+
1150+
// Verify validation call doesn't include encoding_format
1151+
const callArgs = mockEmbeddingsCreate.mock.calls[0][0]
1152+
expect(callArgs).not.toHaveProperty("encoding_format")
1153+
})
1154+
1155+
it("should handle direct HTTP requests for Gemini full URLs without encoding_format", async () => {
1156+
const geminiFullUrl = "https://generativelanguage.googleapis.com/v1beta/openai/embeddings"
1157+
const fullUrlEmbedder = new OpenAICompatibleEmbedder(geminiFullUrl, geminiApiKey, geminiModelId)
1158+
1159+
const mockFetch = vitest.fn().mockResolvedValue({
1160+
ok: true,
1161+
json: async () => ({
1162+
data: [{ embedding: [0.1, 0.2, 0.3] }],
1163+
usage: { prompt_tokens: 10, total_tokens: 15 },
1164+
}),
1165+
})
1166+
global.fetch = mockFetch
1167+
1168+
await fullUrlEmbedder.createEmbeddings(["test"])
1169+
1170+
// Check that the request body doesn't include encoding_format
1171+
expect(mockFetch).toHaveBeenCalledWith(
1172+
geminiFullUrl,
1173+
expect.objectContaining({
1174+
method: "POST",
1175+
body: JSON.stringify({
1176+
input: ["test"],
1177+
model: geminiModelId,
1178+
// encoding_format should NOT be present
1179+
}),
1180+
}),
1181+
)
1182+
1183+
// Verify the actual body content
1184+
const callArgs = mockFetch.mock.calls[0][1]
1185+
const bodyContent = JSON.parse(callArgs.body)
1186+
expect(bodyContent).not.toHaveProperty("encoding_format")
1187+
})
1188+
})
10541189
})

src/services/code-index/embedders/openai-compatible.ts

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
3838
private readonly apiKey: string
3939
private readonly isFullUrl: boolean
4040
private readonly maxItemTokens: number
41+
private readonly isGeminiEndpoint: boolean
4142

4243
// Global rate limiting state shared across all instances
4344
private static globalRateLimitState = {
@@ -73,6 +74,7 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
7374
this.defaultModelId = modelId || getDefaultModelId("openai-compatible")
7475
// Cache the URL type check for performance
7576
this.isFullUrl = this.isFullEndpointUrl(baseUrl)
77+
this.isGeminiEndpoint = this.isGeminiUrl(baseUrl)
7678
this.maxItemTokens = maxItemTokens || MAX_ITEM_TOKENS
7779
}
7880

@@ -182,6 +184,15 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
182184
return patterns.some((pattern) => pattern.test(url))
183185
}
184186

187+
/**
188+
* Determines if the provided URL is a Google Gemini endpoint
189+
* @param url The URL to check
190+
* @returns true if it's a Gemini endpoint
191+
*/
192+
private isGeminiUrl(url: string): boolean {
193+
return url.includes("generativelanguage.googleapis.com")
194+
}
195+
185196
/**
186197
* Makes a direct HTTP request to the embeddings endpoint
187198
* Used when the user provides a full endpoint URL (e.g., Azure OpenAI with query parameters)
@@ -204,11 +215,19 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
204215
"api-key": this.apiKey,
205216
Authorization: `Bearer ${this.apiKey}`,
206217
},
207-
body: JSON.stringify({
208-
input: batchTexts,
209-
model: model,
210-
encoding_format: "base64",
211-
}),
218+
body: JSON.stringify(
219+
this.isGeminiEndpoint
220+
? {
221+
input: batchTexts,
222+
model: model,
223+
// Gemini doesn't support encoding_format parameter
224+
}
225+
: {
226+
input: batchTexts,
227+
model: model,
228+
encoding_format: "base64",
229+
},
230+
),
212231
})
213232

214233
if (!response || !response.ok) {
@@ -263,14 +282,23 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
263282
response = await this.makeDirectEmbeddingRequest(this.baseUrl, batchTexts, model)
264283
} else {
265284
// Use OpenAI SDK for base URLs
266-
response = (await this.embeddingsClient.embeddings.create({
285+
const embeddingParams: any = {
267286
input: batchTexts,
268287
model: model,
269-
// OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256
270-
// when processing numeric arrays, which breaks compatibility with models using larger dimensions.
271-
// By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves.
272-
encoding_format: "base64",
273-
})) as OpenAIEmbeddingResponse
288+
}
289+
290+
// Only add encoding_format for non-Gemini endpoints
291+
// OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256
292+
// when processing numeric arrays, which breaks compatibility with models using larger dimensions.
293+
// By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves.
294+
// However, Gemini doesn't support this parameter, so we exclude it for Gemini endpoints.
295+
if (!this.isGeminiEndpoint) {
296+
embeddingParams.encoding_format = "base64"
297+
}
298+
299+
response = (await this.embeddingsClient.embeddings.create(
300+
embeddingParams,
301+
)) as OpenAIEmbeddingResponse
274302
}
275303

276304
// Convert base64 embeddings to float32 arrays
@@ -365,11 +393,19 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
365393
response = await this.makeDirectEmbeddingRequest(this.baseUrl, testTexts, modelToUse)
366394
} else {
367395
// Test using OpenAI SDK for base URLs
368-
response = (await this.embeddingsClient.embeddings.create({
396+
const embeddingParams: any = {
369397
input: testTexts,
370398
model: modelToUse,
371-
encoding_format: "base64",
372-
})) as OpenAIEmbeddingResponse
399+
}
400+
401+
// Only add encoding_format for non-Gemini endpoints
402+
if (!this.isGeminiEndpoint) {
403+
embeddingParams.encoding_format = "base64"
404+
}
405+
406+
response = (await this.embeddingsClient.embeddings.create(
407+
embeddingParams,
408+
)) as OpenAIEmbeddingResponse
373409
}
374410

375411
// Check if we got a valid response

0 commit comments

Comments
 (0)