Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
})
})
64 changes: 50 additions & 14 deletions src/services/code-index/embedders/openai-compatible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
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 = {
Expand Down Expand Up @@ -73,6 +74,7 @@
this.defaultModelId = modelId || getDefaultModelId("openai-compatible")
// Cache the URL type check for performance
this.isFullUrl = this.isFullEndpointUrl(baseUrl)
this.isGeminiEndpoint = this.isGeminiUrl(baseUrl)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work documenting why we exclude encoding_format for Gemini! Could we also add a class-level comment or constructor documentation mentioning that this class handles both standard OpenAI-compatible endpoints and Google Gemini endpoints with their specific requirements? This would help future maintainers understand the dual purpose of this class.

this.maxItemTokens = maxItemTokens || MAX_ITEM_TOKENS
}

Expand Down Expand Up @@ -182,6 +184,15 @@
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")

Check failure

Code scanning / CodeQL

Incomplete URL substring sanitization High

'
generativelanguage.googleapis.com
' can be anywhere in the URL, and arbitrary hosts may come before or after it.

Copilot Autofix

AI 2 months ago

To robustly detect if the provided URL belongs to the Google Gemini API, we should parse the URL and inspect the hostname (not the full string) for an exact match or subdomain match. This avoids the scenario where the substring appears elsewhere in the URL (path, query, etc).

  • Parse the input URL using the standard URL class (available in Node.js v10+ and modern browsers).
  • Extract the hostname (e.g., generativelanguage.googleapis.com or v1.generativelanguage.googleapis.com).
  • Accept hostnames that either exactly match generativelanguage.googleapis.com or end with .generativelanguage.googleapis.com (to account for subdomains).
  • Implement this purely within the isGeminiUrl method. Add a try-catch to handle invalid URLs gracefully and return false if parsing fails.
  • No additional package imports are required, as URL is a standard global object.

The required change is only within the method isGeminiUrl in src/services/code-index/embedders/openai-compatible.ts.


Suggested changeset 1
src/services/code-index/embedders/openai-compatible.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/src/services/code-index/embedders/openai-compatible.ts b/src/services/code-index/embedders/openai-compatible.ts
--- a/src/services/code-index/embedders/openai-compatible.ts
+++ b/src/services/code-index/embedders/openai-compatible.ts
@@ -190,7 +190,15 @@
 	 * @returns true if it's a Gemini endpoint
 	 */
 	private isGeminiUrl(url: string): boolean {
-		return url.includes("generativelanguage.googleapis.com")
+		try {
+			const { hostname } = new URL(url);
+			return (
+				hostname === "generativelanguage.googleapis.com" ||
+				hostname.endsWith(".generativelanguage.googleapis.com")
+			);
+		} catch {
+			return false;
+		}
 	}
 
 	/**
EOF
@@ -190,7 +190,15 @@
* @returns true if it's a Gemini endpoint
*/
private isGeminiUrl(url: string): boolean {
return url.includes("generativelanguage.googleapis.com")
try {
const { hostname } = new URL(url);
return (
hostname === "generativelanguage.googleapis.com" ||
hostname.endsWith(".generativelanguage.googleapis.com")
);
} catch {
return false;
}
}

/**
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is using url.includes("generativelanguage.googleapis.com") robust enough for detection? This would match URLs like https://evil.com/redirect?to=generativelanguage.googleapis.com. Consider using a more specific pattern like checking if the URL starts with the Gemini domain or using a regex pattern:

Suggested change
return url.includes("generativelanguage.googleapis.com")
private isGeminiUrl(url: string): boolean {
return url.startsWith("https://generativelanguage.googleapis.com/") ||
url.startsWith("http://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)
Expand All @@ -204,11 +215,19 @@
"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) {
Expand Down Expand Up @@ -263,14 +282,23 @@
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) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we add more provider-specific quirks, would it make sense to extract this provider detection logic into a separate utility? Something like a ProviderDetector class that could handle all provider-specific logic? Though this might be premature optimization at this point - just something to consider if we keep adding more provider-specific conditions.

embeddingParams.encoding_format = "base64"
}

response = (await this.embeddingsClient.embeddings.create(
embeddingParams,
)) as OpenAIEmbeddingResponse
}

// Convert base64 embeddings to float32 arrays
Expand Down Expand Up @@ -365,11 +393,19 @@
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
Expand Down
Loading