mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
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
This commit is contained in:
parent
247da38b02
commit
22abb2c45b
2 changed files with 185 additions and 14 deletions
|
|
@ -1051,4 +1051,139 @@ describe("OpenAICompatibleEmbedder", () => {
|
|||
expect(result.error).toBe("embeddings:validation.configurationError")
|
||||
})
|
||||
})
|
||||
|
||||
describe("Gemini compatibility", () => {
|
||||
let geminiEmbedder: OpenAICompatibleEmbedder
|
||||
const geminiBaseUrl = "https://generativelanguage.googleapis.com/v1beta/openai/"
|
||||
const geminiApiKey = "test-gemini-api-key"
|
||||
const geminiModelId = "gemini-embedding-001"
|
||||
|
||||
beforeEach(() => {
|
||||
vitest.clearAllMocks()
|
||||
geminiEmbedder = new OpenAICompatibleEmbedder(geminiBaseUrl, geminiApiKey, geminiModelId)
|
||||
})
|
||||
|
||||
it("should NOT include encoding_format for Gemini endpoints", async () => {
|
||||
const testTexts = ["Hello world"]
|
||||
const mockResponse = {
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
usage: { prompt_tokens: 10, total_tokens: 15 },
|
||||
}
|
||||
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
|
||||
|
||||
await geminiEmbedder.createEmbeddings(testTexts)
|
||||
|
||||
// Verify that encoding_format is NOT included for Gemini
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
|
||||
input: testTexts,
|
||||
model: geminiModelId,
|
||||
// encoding_format should NOT be present
|
||||
})
|
||||
|
||||
// Verify the call doesn't have encoding_format property
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0]
|
||||
expect(callArgs).not.toHaveProperty("encoding_format")
|
||||
})
|
||||
|
||||
it("should still include encoding_format for non-Gemini OpenAI-compatible endpoints", async () => {
|
||||
// Create a non-Gemini embedder
|
||||
const regularEmbedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
|
||||
|
||||
const testTexts = ["Hello world"]
|
||||
const mockResponse = {
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
usage: { prompt_tokens: 10, total_tokens: 15 },
|
||||
}
|
||||
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
|
||||
|
||||
await regularEmbedder.createEmbeddings(testTexts)
|
||||
|
||||
// Verify that encoding_format IS included for non-Gemini endpoints
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
|
||||
input: testTexts,
|
||||
model: testModelId,
|
||||
encoding_format: "base64",
|
||||
})
|
||||
})
|
||||
|
||||
it("should correctly identify Gemini URLs", () => {
|
||||
const geminiUrls = [
|
||||
"https://generativelanguage.googleapis.com/v1beta/openai/",
|
||||
"https://generativelanguage.googleapis.com/v1/openai/",
|
||||
"https://generativelanguage.googleapis.com/v2/embeddings",
|
||||
]
|
||||
|
||||
geminiUrls.forEach((url) => {
|
||||
const embedder = new OpenAICompatibleEmbedder(url, geminiApiKey, geminiModelId)
|
||||
const isGemini = (embedder as any).isGeminiUrl(url)
|
||||
expect(isGemini).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
it("should not identify non-Gemini URLs as Gemini", () => {
|
||||
const nonGeminiUrls = [
|
||||
"https://api.openai.com/v1",
|
||||
"https://api.example.com/embeddings",
|
||||
"https://myinstance.openai.azure.com/openai/deployments/my-deployment/embeddings",
|
||||
"http://localhost:8080",
|
||||
]
|
||||
|
||||
nonGeminiUrls.forEach((url) => {
|
||||
const embedder = new OpenAICompatibleEmbedder(url, testApiKey, testModelId)
|
||||
const isGemini = (embedder as any).isGeminiUrl(url)
|
||||
expect(isGemini).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
it("should validate Gemini configuration without encoding_format", async () => {
|
||||
const mockResponse = {
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}
|
||||
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
|
||||
|
||||
const result = await geminiEmbedder.validateConfiguration()
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.error).toBeUndefined()
|
||||
|
||||
// Verify validation call doesn't include encoding_format
|
||||
const callArgs = mockEmbeddingsCreate.mock.calls[0][0]
|
||||
expect(callArgs).not.toHaveProperty("encoding_format")
|
||||
})
|
||||
|
||||
it("should handle direct HTTP requests for Gemini full URLs without encoding_format", async () => {
|
||||
const geminiFullUrl = "https://generativelanguage.googleapis.com/v1beta/openai/embeddings"
|
||||
const fullUrlEmbedder = new OpenAICompatibleEmbedder(geminiFullUrl, geminiApiKey, geminiModelId)
|
||||
|
||||
const mockFetch = vitest.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
usage: { prompt_tokens: 10, total_tokens: 15 },
|
||||
}),
|
||||
})
|
||||
global.fetch = mockFetch
|
||||
|
||||
await fullUrlEmbedder.createEmbeddings(["test"])
|
||||
|
||||
// Check that the request body doesn't include encoding_format
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
geminiFullUrl,
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
input: ["test"],
|
||||
model: geminiModelId,
|
||||
// encoding_format should NOT be present
|
||||
}),
|
||||
}),
|
||||
)
|
||||
|
||||
// Verify the actual body content
|
||||
const callArgs = mockFetch.mock.calls[0][1]
|
||||
const bodyContent = JSON.parse(callArgs.body)
|
||||
expect(bodyContent).not.toHaveProperty("encoding_format")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
private readonly apiKey: string
|
||||
private readonly isFullUrl: boolean
|
||||
private readonly maxItemTokens: number
|
||||
private readonly isGeminiEndpoint: boolean
|
||||
|
||||
// Global rate limiting state shared across all instances
|
||||
private static globalRateLimitState = {
|
||||
|
|
@ -73,6 +74,7 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
this.defaultModelId = modelId || getDefaultModelId("openai-compatible")
|
||||
// Cache the URL type check for performance
|
||||
this.isFullUrl = this.isFullEndpointUrl(baseUrl)
|
||||
this.isGeminiEndpoint = this.isGeminiUrl(baseUrl)
|
||||
this.maxItemTokens = maxItemTokens || MAX_ITEM_TOKENS
|
||||
}
|
||||
|
||||
|
|
@ -182,6 +184,15 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
return patterns.some((pattern) => pattern.test(url))
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided URL is a Google Gemini endpoint
|
||||
* @param url The URL to check
|
||||
* @returns true if it's a Gemini endpoint
|
||||
*/
|
||||
private isGeminiUrl(url: string): boolean {
|
||||
return url.includes("generativelanguage.googleapis.com")
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a direct HTTP request to the embeddings endpoint
|
||||
* 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 {
|
|||
"api-key": this.apiKey,
|
||||
Authorization: `Bearer ${this.apiKey}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: batchTexts,
|
||||
model: model,
|
||||
encoding_format: "base64",
|
||||
}),
|
||||
body: JSON.stringify(
|
||||
this.isGeminiEndpoint
|
||||
? {
|
||||
input: batchTexts,
|
||||
model: model,
|
||||
// Gemini doesn't support encoding_format parameter
|
||||
}
|
||||
: {
|
||||
input: batchTexts,
|
||||
model: model,
|
||||
encoding_format: "base64",
|
||||
},
|
||||
),
|
||||
})
|
||||
|
||||
if (!response || !response.ok) {
|
||||
|
|
@ -263,14 +282,23 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
response = await this.makeDirectEmbeddingRequest(this.baseUrl, batchTexts, model)
|
||||
} else {
|
||||
// Use OpenAI SDK for base URLs
|
||||
response = (await this.embeddingsClient.embeddings.create({
|
||||
const embeddingParams: any = {
|
||||
input: batchTexts,
|
||||
model: model,
|
||||
// OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256
|
||||
// when processing numeric arrays, which breaks compatibility with models using larger dimensions.
|
||||
// By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves.
|
||||
encoding_format: "base64",
|
||||
})) as OpenAIEmbeddingResponse
|
||||
}
|
||||
|
||||
// Only add encoding_format for non-Gemini endpoints
|
||||
// OpenAI package (as of v4.78.1) has a parsing issue that truncates embedding dimensions to 256
|
||||
// when processing numeric arrays, which breaks compatibility with models using larger dimensions.
|
||||
// By requesting base64 encoding, we bypass the package's parser and handle decoding ourselves.
|
||||
// However, Gemini doesn't support this parameter, so we exclude it for Gemini endpoints.
|
||||
if (!this.isGeminiEndpoint) {
|
||||
embeddingParams.encoding_format = "base64"
|
||||
}
|
||||
|
||||
response = (await this.embeddingsClient.embeddings.create(
|
||||
embeddingParams,
|
||||
)) as OpenAIEmbeddingResponse
|
||||
}
|
||||
|
||||
// Convert base64 embeddings to float32 arrays
|
||||
|
|
@ -365,11 +393,19 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
response = await this.makeDirectEmbeddingRequest(this.baseUrl, testTexts, modelToUse)
|
||||
} else {
|
||||
// Test using OpenAI SDK for base URLs
|
||||
response = (await this.embeddingsClient.embeddings.create({
|
||||
const embeddingParams: any = {
|
||||
input: testTexts,
|
||||
model: modelToUse,
|
||||
encoding_format: "base64",
|
||||
})) as OpenAIEmbeddingResponse
|
||||
}
|
||||
|
||||
// Only add encoding_format for non-Gemini endpoints
|
||||
if (!this.isGeminiEndpoint) {
|
||||
embeddingParams.encoding_format = "base64"
|
||||
}
|
||||
|
||||
response = (await this.embeddingsClient.embeddings.create(
|
||||
embeddingParams,
|
||||
)) as OpenAIEmbeddingResponse
|
||||
}
|
||||
|
||||
// Check if we got a valid response
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue