mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-11 22:51:26 +00:00
Merge 3b1f356757 into 7adbfec2a4
This commit is contained in:
commit
dbbc7e00a2
15 changed files with 364 additions and 58 deletions
|
|
@ -347,6 +347,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
|
||||
// Mock service factory instance
|
||||
mockServiceFactoryInstance = {
|
||||
createEmbedder: vi.fn().mockReturnValue(mockEmbedder),
|
||||
createServices: vi.fn().mockReturnValue({
|
||||
embedder: mockEmbedder,
|
||||
vectorStore: mockVectorStore,
|
||||
|
|
@ -389,9 +390,9 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
await (manager as any)._recreateServices()
|
||||
|
||||
// Assert
|
||||
expect(mockServiceFactoryInstance.createEmbedder).toHaveBeenCalled()
|
||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder)
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
||||
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
|
||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
|
||||
expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String))
|
||||
})
|
||||
|
||||
|
|
@ -408,9 +409,8 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
)
|
||||
|
||||
// Assert other expectations
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
||||
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
|
||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
|
||||
expect(mockServiceFactoryInstance.createEmbedder).toHaveBeenCalled()
|
||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder)
|
||||
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
|
||||
"Error",
|
||||
"embeddings:validation.authenticationFailed",
|
||||
|
|
@ -431,9 +431,8 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
)
|
||||
|
||||
// Assert other expectations
|
||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
||||
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
|
||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
|
||||
expect(mockServiceFactoryInstance.createEmbedder).toHaveBeenCalled()
|
||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder)
|
||||
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
|
||||
"Error",
|
||||
"embeddings:validation.configurationError",
|
||||
|
|
@ -441,8 +440,8 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
})
|
||||
|
||||
it("should handle embedder creation failure", async () => {
|
||||
// Arrange
|
||||
mockServiceFactoryInstance.createServices.mockImplementation(() => {
|
||||
// Arrange - createEmbedder is now called before createServices
|
||||
mockServiceFactoryInstance.createEmbedder.mockImplementation(() => {
|
||||
throw new Error("Invalid configuration")
|
||||
})
|
||||
|
||||
|
|
@ -540,6 +539,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
|||
it("should allow re-initialization after recovery", async () => {
|
||||
// Setup mock for re-initialization
|
||||
const mockServiceFactoryInstance = {
|
||||
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
|
||||
createServices: vi.fn().mockReturnValue({
|
||||
embedder: { embedderInfo: { name: "openai" } },
|
||||
vectorStore: {},
|
||||
|
|
|
|||
|
|
@ -364,6 +364,125 @@ describe("CodeIndexServiceFactory", () => {
|
|||
mockGetDefaultModelId.mockReturnValue("default-model")
|
||||
})
|
||||
|
||||
it("should prioritize detectedDimension over all other dimension sources", () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
embedderProvider: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
modelDimension: 1024, // Manual config should be ignored
|
||||
qdrantUrl: "http://localhost:6333",
|
||||
qdrantApiKey: "test-key",
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
mockGetModelDimension.mockReturnValue(768) // Profile dimension should be ignored
|
||||
|
||||
// Act - pass detected dimension from validation
|
||||
factory.createVectorStore(4096)
|
||||
|
||||
// Assert - should use detected dimension (4096), not profile (768) or manual (1024)
|
||||
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
|
||||
"/test/workspace",
|
||||
"http://localhost:6333",
|
||||
4096, // Auto-detected dimension takes priority
|
||||
"test-key",
|
||||
)
|
||||
})
|
||||
|
||||
it("should use detected dimension from Ollama embedder", () => {
|
||||
// Arrange - simulates Ollama with qwen3-embedding returning 4096 dimensions
|
||||
const testConfig = {
|
||||
embedderProvider: "ollama",
|
||||
modelId: "qwen3-embedding",
|
||||
modelDimension: 1536, // User's incorrect manual config
|
||||
qdrantUrl: "http://localhost:6333",
|
||||
qdrantApiKey: "test-key",
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
mockGetModelDimension.mockReturnValue(undefined) // Unknown model
|
||||
|
||||
// Act - pass detected dimension from validation (like the issue scenario)
|
||||
factory.createVectorStore(4096)
|
||||
|
||||
// Assert - should use auto-detected 4096, not user's incorrect 1536
|
||||
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
|
||||
"/test/workspace",
|
||||
"http://localhost:6333",
|
||||
4096,
|
||||
"test-key",
|
||||
)
|
||||
})
|
||||
|
||||
it("should fall back to profile dimension when detected dimension is not provided", () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
embedderProvider: "openai",
|
||||
modelId: "text-embedding-3-large",
|
||||
qdrantUrl: "http://localhost:6333",
|
||||
qdrantApiKey: "test-key",
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
mockGetModelDimension.mockReturnValue(3072)
|
||||
|
||||
// Act - no detected dimension provided
|
||||
factory.createVectorStore()
|
||||
|
||||
// Assert - should use profile dimension
|
||||
expect(mockGetModelDimension).toHaveBeenCalledWith("openai", "text-embedding-3-large")
|
||||
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
|
||||
"/test/workspace",
|
||||
"http://localhost:6333",
|
||||
3072,
|
||||
"test-key",
|
||||
)
|
||||
})
|
||||
|
||||
it("should fall back to manual dimension when detected and profile are unavailable", () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
embedderProvider: "openai-compatible",
|
||||
modelId: "unknown-model",
|
||||
modelDimension: 2048,
|
||||
qdrantUrl: "http://localhost:6333",
|
||||
qdrantApiKey: "test-key",
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
mockGetModelDimension.mockReturnValue(undefined)
|
||||
|
||||
// Act - no detected dimension, no profile dimension
|
||||
factory.createVectorStore()
|
||||
|
||||
// Assert - should use manual dimension
|
||||
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
|
||||
"/test/workspace",
|
||||
"http://localhost:6333",
|
||||
2048,
|
||||
"test-key",
|
||||
)
|
||||
})
|
||||
|
||||
it("should ignore zero or negative detected dimension", () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
embedderProvider: "openai",
|
||||
modelId: "text-embedding-3-small",
|
||||
qdrantUrl: "http://localhost:6333",
|
||||
qdrantApiKey: "test-key",
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
mockGetModelDimension.mockReturnValue(1536)
|
||||
|
||||
// Act - pass invalid detected dimension
|
||||
factory.createVectorStore(0)
|
||||
|
||||
// Assert - should fall back to profile dimension
|
||||
expect(MockedQdrantVectorStore).toHaveBeenCalledWith(
|
||||
"/test/workspace",
|
||||
"http://localhost:6333",
|
||||
1536,
|
||||
"test-key",
|
||||
)
|
||||
})
|
||||
|
||||
it("should use config.modelId for OpenAI provider", () => {
|
||||
// Arrange
|
||||
const testModelId = "text-embedding-3-large"
|
||||
|
|
@ -689,6 +808,58 @@ describe("CodeIndexServiceFactory", () => {
|
|||
}
|
||||
})
|
||||
|
||||
it("should return detectedDimension from embedder validation", async () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
embedderProvider: "ollama",
|
||||
modelId: "qwen3-embedding",
|
||||
ollamaOptions: {
|
||||
ollamaBaseUrl: "http://localhost:11434",
|
||||
},
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
MockedCodeIndexOllamaEmbedder.mockImplementation(() => mockEmbedderInstance)
|
||||
// Mock embedder returning detected dimension
|
||||
mockEmbedderInstance.validateConfiguration.mockResolvedValue({
|
||||
valid: true,
|
||||
detectedDimension: 4096,
|
||||
})
|
||||
|
||||
// Act
|
||||
const embedder = factory.createEmbedder()
|
||||
const result = await factory.validateEmbedder(embedder)
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({ valid: true, detectedDimension: 4096 })
|
||||
expect(mockEmbedderInstance.validateConfiguration).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should return detectedDimension from base64 embedding validation", async () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
embedderProvider: "openai-compatible",
|
||||
modelId: "custom-model",
|
||||
openAiCompatibleOptions: {
|
||||
baseUrl: "https://api.example.com/v1",
|
||||
apiKey: "test-api-key",
|
||||
},
|
||||
}
|
||||
mockConfigManager.getConfig.mockReturnValue(testConfig as any)
|
||||
MockedOpenAICompatibleEmbedder.mockImplementation(() => mockEmbedderInstance)
|
||||
// Mock embedder returning detected dimension from base64 parsing
|
||||
mockEmbedderInstance.validateConfiguration.mockResolvedValue({
|
||||
valid: true,
|
||||
detectedDimension: 1536,
|
||||
})
|
||||
|
||||
// Act
|
||||
const embedder = factory.createEmbedder()
|
||||
const result = await factory.validateEmbedder(embedder)
|
||||
|
||||
// Assert
|
||||
expect(result).toEqual({ valid: true, detectedDimension: 1536 })
|
||||
})
|
||||
|
||||
it("should validate OpenAI embedder successfully", async () => {
|
||||
// Arrange
|
||||
const testConfig = {
|
||||
|
|
|
|||
|
|
@ -196,6 +196,7 @@ describe("CodeIndexOllamaEmbedder", () => {
|
|||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.detectedDimension).toBe(3) // Auto-detected from test embedding
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2)
|
||||
|
||||
// Check first call (GET /api/tags)
|
||||
|
|
@ -214,6 +215,38 @@ describe("CodeIndexOllamaEmbedder", () => {
|
|||
expect(secondCall[1]?.signal).toBeDefined() // AbortSignal for timeout
|
||||
})
|
||||
|
||||
it("should detect dimension from realistic embedding size", async () => {
|
||||
// Mock successful /api/tags call
|
||||
mockFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
models: [{ name: "nomic-embed-text:latest" }],
|
||||
}),
|
||||
} as Response),
|
||||
)
|
||||
|
||||
// Mock successful /api/embed test call with 4096-dimension embedding (like qwen3-embedding)
|
||||
const largeEmbedding = new Array(4096).fill(0).map((_, i) => i * 0.001)
|
||||
mockFetch.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () =>
|
||||
Promise.resolve({
|
||||
embeddings: [largeEmbedding],
|
||||
}),
|
||||
} as Response),
|
||||
)
|
||||
|
||||
const result = await embedder.validateConfiguration()
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.detectedDimension).toBe(4096)
|
||||
})
|
||||
|
||||
it("should fail validation when service is not available", async () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
||||
|
||||
|
|
|
|||
|
|
@ -978,6 +978,7 @@ describe("OpenAICompatibleEmbedder", () => {
|
|||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.detectedDimension).toBe(3) // Auto-detected from array embedding
|
||||
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
|
||||
input: ["test"],
|
||||
model: testModelId,
|
||||
|
|
@ -1003,6 +1004,7 @@ describe("OpenAICompatibleEmbedder", () => {
|
|||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.error).toBeUndefined()
|
||||
expect(result.detectedDimension).toBe(3) // Auto-detected from array embedding
|
||||
expect(mockFetch).toHaveBeenCalledWith(
|
||||
fullUrl,
|
||||
expect.objectContaining({
|
||||
|
|
@ -1014,6 +1016,25 @@ describe("OpenAICompatibleEmbedder", () => {
|
|||
)
|
||||
})
|
||||
|
||||
it("should detect dimension from base64 encoded embedding", async () => {
|
||||
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
|
||||
|
||||
// Create a 1536-dimension embedding as base64 (like text-embedding-3-small)
|
||||
const embedding = new Float32Array(1536).fill(0.1)
|
||||
const base64String = Buffer.from(embedding.buffer).toString("base64")
|
||||
|
||||
const mockResponse = {
|
||||
data: [{ embedding: base64String }],
|
||||
usage: { prompt_tokens: 2, total_tokens: 2 },
|
||||
}
|
||||
mockEmbeddingsCreate.mockResolvedValue(mockResponse)
|
||||
|
||||
const result = await embedder.validateConfiguration()
|
||||
|
||||
expect(result.valid).toBe(true)
|
||||
expect(result.detectedDimension).toBe(1536) // Auto-detected from base64 (1536 * 4 bytes / 4 = 1536)
|
||||
})
|
||||
|
||||
it("should fail validation with authentication error", async () => {
|
||||
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
|
||||
|
||||
|
|
|
|||
|
|
@ -276,10 +276,11 @@ export class BedrockEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the Bedrock embedder configuration by attempting a minimal embedding request
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the Bedrock embedder configuration by attempting a minimal embedding request.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
return withValidationErrorHandling(async () => {
|
||||
try {
|
||||
// Test with a minimal embedding request
|
||||
|
|
@ -293,7 +294,10 @@ export class BedrockEmbedder implements IEmbedder {
|
|||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
// Get the dimension from the embedding
|
||||
const detectedDimension = result.embedding.length
|
||||
|
||||
return { valid: true, detectedDimension }
|
||||
} catch (error: any) {
|
||||
// Check for specific AWS errors
|
||||
if (error.name === "UnrecognizedClientException") {
|
||||
|
|
|
|||
|
|
@ -84,10 +84,11 @@ export class GeminiEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the Gemini embedder configuration by delegating to the underlying OpenAI-compatible embedder.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
try {
|
||||
// Delegate validation to the OpenAI-compatible embedder
|
||||
// The error messages will be specific to Gemini since we're using Gemini's base URL
|
||||
|
|
|
|||
|
|
@ -62,10 +62,11 @@ export class MistralEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the Mistral embedder configuration by delegating to the underlying OpenAI-compatible embedder
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the Mistral embedder configuration by delegating to the underlying OpenAI-compatible embedder.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
try {
|
||||
// Delegate validation to the OpenAI-compatible embedder
|
||||
// The error messages will be specific to Mistral since we're using Mistral's base URL
|
||||
|
|
|
|||
|
|
@ -138,10 +138,11 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the Ollama embedder configuration by checking service availability and model existence
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the Ollama embedder configuration by checking service availability and model existence.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
return withValidationErrorHandling(
|
||||
async () => {
|
||||
// First check if Ollama service is running by trying to list models
|
||||
|
|
@ -228,7 +229,19 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
|
|||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
// Parse the test response to get the embedding dimension
|
||||
const testData = await testResponse.json()
|
||||
const embeddings = testData.embeddings
|
||||
let detectedDimension: number | undefined
|
||||
|
||||
if (embeddings && Array.isArray(embeddings) && embeddings.length > 0) {
|
||||
const firstEmbedding = embeddings[0]
|
||||
if (Array.isArray(firstEmbedding)) {
|
||||
detectedDimension = firstEmbedding.length
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, detectedDimension }
|
||||
},
|
||||
"ollama",
|
||||
{
|
||||
|
|
|
|||
|
|
@ -357,10 +357,11 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the OpenAI-compatible embedder configuration by testing endpoint connectivity and API key
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the OpenAI-compatible embedder configuration by testing endpoint connectivity and API key.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
return withValidationErrorHandling(async () => {
|
||||
try {
|
||||
// Test with a minimal embedding request
|
||||
|
|
@ -389,7 +390,20 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
|||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
// Convert base64 embedding to get the actual dimension
|
||||
let detectedDimension: number | undefined
|
||||
const firstItem = response.data[0]
|
||||
if (firstItem?.embedding) {
|
||||
if (typeof firstItem.embedding === "string") {
|
||||
// Decode base64 to get float32 array length
|
||||
const buffer = Buffer.from(firstItem.embedding, "base64")
|
||||
detectedDimension = buffer.byteLength / 4 // 4 bytes per float32
|
||||
} else if (Array.isArray(firstItem.embedding)) {
|
||||
detectedDimension = firstItem.embedding.length
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, detectedDimension }
|
||||
} catch (error) {
|
||||
// Capture telemetry for validation errors
|
||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
||||
|
|
|
|||
|
|
@ -187,10 +187,11 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the OpenAI embedder configuration by attempting a minimal embedding request
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the OpenAI embedder configuration by attempting a minimal embedding request.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
return withValidationErrorHandling(async () => {
|
||||
try {
|
||||
// Test with a minimal embedding request
|
||||
|
|
@ -207,7 +208,10 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
|
|||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
// Get the dimension from the first embedding
|
||||
const detectedDimension = response.data[0]?.embedding?.length
|
||||
|
||||
return { valid: true, detectedDimension }
|
||||
} catch (error) {
|
||||
// Capture telemetry for validation errors
|
||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
||||
|
|
|
|||
|
|
@ -286,10 +286,11 @@ export class OpenRouterEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the OpenRouter embedder configuration by testing API connectivity
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the OpenRouter embedder configuration by testing API connectivity.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
return withValidationErrorHandling(async () => {
|
||||
try {
|
||||
// Test with a minimal embedding request
|
||||
|
|
@ -324,7 +325,20 @@ export class OpenRouterEmbedder implements IEmbedder {
|
|||
}
|
||||
}
|
||||
|
||||
return { valid: true }
|
||||
// Detect the embedding dimension from the response
|
||||
let detectedDimension: number | undefined
|
||||
const firstItem = response.data[0]
|
||||
if (firstItem?.embedding) {
|
||||
if (typeof firstItem.embedding === "string") {
|
||||
// Decode base64 to get float32 array length
|
||||
const buffer = Buffer.from(firstItem.embedding, "base64")
|
||||
detectedDimension = buffer.byteLength / 4 // 4 bytes per float32
|
||||
} else if (Array.isArray(firstItem.embedding)) {
|
||||
detectedDimension = firstItem.embedding.length
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: true, detectedDimension }
|
||||
} catch (error) {
|
||||
// Capture telemetry for validation errors
|
||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
||||
|
|
|
|||
|
|
@ -71,10 +71,11 @@ export class VercelAiGatewayEmbedder implements IEmbedder {
|
|||
}
|
||||
|
||||
/**
|
||||
* Validates the Vercel AI Gateway embedder configuration by delegating to the underlying OpenAI-compatible embedder
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Validates the Vercel AI Gateway embedder configuration by delegating to the underlying OpenAI-compatible embedder.
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
try {
|
||||
// Delegate validation to the OpenAI-compatible embedder
|
||||
// The error messages will be specific to Vercel AI Gateway since we're using Vercel's base URL
|
||||
|
|
|
|||
|
|
@ -13,9 +13,10 @@ export interface IEmbedder {
|
|||
|
||||
/**
|
||||
* Validates the embedder configuration by testing connectivity and credentials.
|
||||
* @returns Promise resolving to validation result with success status and optional error message
|
||||
* Also detects the actual embedding dimension from a test embedding.
|
||||
* @returns Promise resolving to validation result with success status, optional error message, and detected dimension
|
||||
*/
|
||||
validateConfiguration(): Promise<{ valid: boolean; error?: string }>
|
||||
validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }>
|
||||
|
||||
get embedderInfo(): EmbedderInfo
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,15 +390,10 @@ export class CodeIndexManager {
|
|||
const rooIgnoreController = new RooIgnoreController(workspacePath)
|
||||
await rooIgnoreController.initialize()
|
||||
|
||||
// (Re)Create shared service instances
|
||||
const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
|
||||
this.context,
|
||||
this._cacheManager!,
|
||||
ignoreInstance,
|
||||
rooIgnoreController,
|
||||
)
|
||||
// Create embedder first to validate and detect embedding dimension
|
||||
const embedder = this._serviceFactory.createEmbedder()
|
||||
|
||||
// Validate embedder configuration before proceeding
|
||||
// Validate embedder configuration and detect actual embedding dimension
|
||||
const validationResult = await this._serviceFactory.validateEmbedder(embedder)
|
||||
if (!validationResult.valid) {
|
||||
const errorMessage = validationResult.error || "Embedder configuration validation failed"
|
||||
|
|
@ -406,6 +401,20 @@ export class CodeIndexManager {
|
|||
throw new Error(errorMessage)
|
||||
}
|
||||
|
||||
// Use the auto-detected dimension if available
|
||||
// This ensures we always use the actual dimension from the model,
|
||||
// preventing mismatches between configured and actual dimensions (Issue #10991)
|
||||
const detectedDimension = validationResult.detectedDimension
|
||||
|
||||
// (Re)Create shared service instances with the detected dimension
|
||||
const { vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
|
||||
this.context,
|
||||
this._cacheManager!,
|
||||
ignoreInstance,
|
||||
rooIgnoreController,
|
||||
detectedDimension,
|
||||
)
|
||||
|
||||
// (Re)Initialize orchestrator
|
||||
this._orchestrator = new CodeIndexOrchestrator(
|
||||
this._configManager!,
|
||||
|
|
|
|||
|
|
@ -112,10 +112,13 @@ export class CodeIndexServiceFactory {
|
|||
|
||||
/**
|
||||
* Validates an embedder instance to ensure it's properly configured.
|
||||
* Also captures the detected embedding dimension from the test embedding.
|
||||
* @param embedder The embedder instance to validate
|
||||
* @returns Promise resolving to validation result
|
||||
* @returns Promise resolving to validation result with optional detected dimension
|
||||
*/
|
||||
public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> {
|
||||
public async validateEmbedder(
|
||||
embedder: IEmbedder,
|
||||
): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||
try {
|
||||
return await embedder.validateConfiguration()
|
||||
} catch (error) {
|
||||
|
|
@ -136,8 +139,10 @@ export class CodeIndexServiceFactory {
|
|||
|
||||
/**
|
||||
* Creates a vector store instance using the current configuration.
|
||||
* @param detectedDimension Optional embedding dimension auto-detected from a test embedding.
|
||||
* When provided, this takes priority over profile-based or manual dimensions.
|
||||
*/
|
||||
public createVectorStore(): IVectorStore {
|
||||
public createVectorStore(detectedDimension?: number): IVectorStore {
|
||||
const config = this.configManager.getConfig()
|
||||
|
||||
const provider = config.embedderProvider as EmbedderProvider
|
||||
|
|
@ -147,12 +152,20 @@ export class CodeIndexServiceFactory {
|
|||
|
||||
let vectorSize: number | undefined
|
||||
|
||||
// First try to get the model-specific dimension from profiles
|
||||
vectorSize = getModelDimension(provider, modelId)
|
||||
// Priority order for vector dimension:
|
||||
// 1. Auto-detected dimension from test embedding (most reliable)
|
||||
// 2. Model-specific dimension from profiles
|
||||
// 3. Manual dimension from config (fallback for unknown models)
|
||||
if (detectedDimension && detectedDimension > 0) {
|
||||
vectorSize = detectedDimension
|
||||
} else {
|
||||
// Try to get the model-specific dimension from profiles
|
||||
vectorSize = getModelDimension(provider, modelId)
|
||||
|
||||
// Only use manual dimension if model doesn't have a built-in dimension
|
||||
if (!vectorSize && config.modelDimension && config.modelDimension > 0) {
|
||||
vectorSize = config.modelDimension
|
||||
// Only use manual dimension if model doesn't have a built-in dimension
|
||||
if (!vectorSize && config.modelDimension && config.modelDimension > 0) {
|
||||
vectorSize = config.modelDimension
|
||||
}
|
||||
}
|
||||
|
||||
if (vectorSize === undefined || vectorSize <= 0) {
|
||||
|
|
@ -230,6 +243,11 @@ export class CodeIndexServiceFactory {
|
|||
|
||||
/**
|
||||
* Creates all required service dependencies if the service is properly configured.
|
||||
* @param context VSCode extension context
|
||||
* @param cacheManager Cache manager instance
|
||||
* @param ignoreInstance Ignore instance for .gitignore
|
||||
* @param rooIgnoreController Optional RooIgnore controller
|
||||
* @param detectedDimension Optional auto-detected embedding dimension from validation
|
||||
* @throws Error if the service is not properly configured
|
||||
*/
|
||||
public createServices(
|
||||
|
|
@ -237,6 +255,7 @@ export class CodeIndexServiceFactory {
|
|||
cacheManager: CacheManager,
|
||||
ignoreInstance: Ignore,
|
||||
rooIgnoreController?: RooIgnoreController,
|
||||
detectedDimension?: number,
|
||||
): {
|
||||
embedder: IEmbedder
|
||||
vectorStore: IVectorStore
|
||||
|
|
@ -249,7 +268,7 @@ export class CodeIndexServiceFactory {
|
|||
}
|
||||
|
||||
const embedder = this.createEmbedder()
|
||||
const vectorStore = this.createVectorStore()
|
||||
const vectorStore = this.createVectorStore(detectedDimension)
|
||||
const parser = codeParser
|
||||
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
|
||||
const fileWatcher = this.createFileWatcher(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue