mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-09 03:17:58 +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
|
// Mock service factory instance
|
||||||
mockServiceFactoryInstance = {
|
mockServiceFactoryInstance = {
|
||||||
|
createEmbedder: vi.fn().mockReturnValue(mockEmbedder),
|
||||||
createServices: vi.fn().mockReturnValue({
|
createServices: vi.fn().mockReturnValue({
|
||||||
embedder: mockEmbedder,
|
embedder: mockEmbedder,
|
||||||
vectorStore: mockVectorStore,
|
vectorStore: mockVectorStore,
|
||||||
|
|
@ -389,9 +390,9 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
||||||
await (manager as any)._recreateServices()
|
await (manager as any)._recreateServices()
|
||||||
|
|
||||||
// Assert
|
// Assert
|
||||||
|
expect(mockServiceFactoryInstance.createEmbedder).toHaveBeenCalled()
|
||||||
|
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder)
|
||||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
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))
|
expect(mockStateManager.setSystemState).not.toHaveBeenCalledWith("Error", expect.any(String))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -408,9 +409,8 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert other expectations
|
// Assert other expectations
|
||||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
expect(mockServiceFactoryInstance.createEmbedder).toHaveBeenCalled()
|
||||||
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
|
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder)
|
||||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
|
|
||||||
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
|
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
|
||||||
"Error",
|
"Error",
|
||||||
"embeddings:validation.authenticationFailed",
|
"embeddings:validation.authenticationFailed",
|
||||||
|
|
@ -431,9 +431,8 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
||||||
)
|
)
|
||||||
|
|
||||||
// Assert other expectations
|
// Assert other expectations
|
||||||
expect(mockServiceFactoryInstance.createServices).toHaveBeenCalled()
|
expect(mockServiceFactoryInstance.createEmbedder).toHaveBeenCalled()
|
||||||
const createdEmbedder = mockServiceFactoryInstance.createServices.mock.results[0].value.embedder
|
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(mockEmbedder)
|
||||||
expect(mockServiceFactoryInstance.validateEmbedder).toHaveBeenCalledWith(createdEmbedder)
|
|
||||||
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
|
expect(mockStateManager.setSystemState).toHaveBeenCalledWith(
|
||||||
"Error",
|
"Error",
|
||||||
"embeddings:validation.configurationError",
|
"embeddings:validation.configurationError",
|
||||||
|
|
@ -441,8 +440,8 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should handle embedder creation failure", async () => {
|
it("should handle embedder creation failure", async () => {
|
||||||
// Arrange
|
// Arrange - createEmbedder is now called before createServices
|
||||||
mockServiceFactoryInstance.createServices.mockImplementation(() => {
|
mockServiceFactoryInstance.createEmbedder.mockImplementation(() => {
|
||||||
throw new Error("Invalid configuration")
|
throw new Error("Invalid configuration")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -540,6 +539,7 @@ describe("CodeIndexManager - handleSettingsChange regression", () => {
|
||||||
it("should allow re-initialization after recovery", async () => {
|
it("should allow re-initialization after recovery", async () => {
|
||||||
// Setup mock for re-initialization
|
// Setup mock for re-initialization
|
||||||
const mockServiceFactoryInstance = {
|
const mockServiceFactoryInstance = {
|
||||||
|
createEmbedder: vi.fn().mockReturnValue({ embedderInfo: { name: "openai" } }),
|
||||||
createServices: vi.fn().mockReturnValue({
|
createServices: vi.fn().mockReturnValue({
|
||||||
embedder: { embedderInfo: { name: "openai" } },
|
embedder: { embedderInfo: { name: "openai" } },
|
||||||
vectorStore: {},
|
vectorStore: {},
|
||||||
|
|
|
||||||
|
|
@ -364,6 +364,125 @@ describe("CodeIndexServiceFactory", () => {
|
||||||
mockGetDefaultModelId.mockReturnValue("default-model")
|
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", () => {
|
it("should use config.modelId for OpenAI provider", () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const testModelId = "text-embedding-3-large"
|
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 () => {
|
it("should validate OpenAI embedder successfully", async () => {
|
||||||
// Arrange
|
// Arrange
|
||||||
const testConfig = {
|
const testConfig = {
|
||||||
|
|
|
||||||
|
|
@ -196,6 +196,7 @@ describe("CodeIndexOllamaEmbedder", () => {
|
||||||
|
|
||||||
expect(result.valid).toBe(true)
|
expect(result.valid).toBe(true)
|
||||||
expect(result.error).toBeUndefined()
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.detectedDimension).toBe(3) // Auto-detected from test embedding
|
||||||
expect(mockFetch).toHaveBeenCalledTimes(2)
|
expect(mockFetch).toHaveBeenCalledTimes(2)
|
||||||
|
|
||||||
// Check first call (GET /api/tags)
|
// Check first call (GET /api/tags)
|
||||||
|
|
@ -214,6 +215,38 @@ describe("CodeIndexOllamaEmbedder", () => {
|
||||||
expect(secondCall[1]?.signal).toBeDefined() // AbortSignal for timeout
|
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 () => {
|
it("should fail validation when service is not available", async () => {
|
||||||
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -978,6 +978,7 @@ describe("OpenAICompatibleEmbedder", () => {
|
||||||
|
|
||||||
expect(result.valid).toBe(true)
|
expect(result.valid).toBe(true)
|
||||||
expect(result.error).toBeUndefined()
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.detectedDimension).toBe(3) // Auto-detected from array embedding
|
||||||
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
|
expect(mockEmbeddingsCreate).toHaveBeenCalledWith({
|
||||||
input: ["test"],
|
input: ["test"],
|
||||||
model: testModelId,
|
model: testModelId,
|
||||||
|
|
@ -1003,6 +1004,7 @@ describe("OpenAICompatibleEmbedder", () => {
|
||||||
|
|
||||||
expect(result.valid).toBe(true)
|
expect(result.valid).toBe(true)
|
||||||
expect(result.error).toBeUndefined()
|
expect(result.error).toBeUndefined()
|
||||||
|
expect(result.detectedDimension).toBe(3) // Auto-detected from array embedding
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
fullUrl,
|
fullUrl,
|
||||||
expect.objectContaining({
|
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 () => {
|
it("should fail validation with authentication error", async () => {
|
||||||
embedder = new OpenAICompatibleEmbedder(testBaseUrl, testApiKey, testModelId)
|
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
|
* Validates the Bedrock embedder configuration by attempting a minimal embedding request.
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||||
return withValidationErrorHandling(async () => {
|
return withValidationErrorHandling(async () => {
|
||||||
try {
|
try {
|
||||||
// Test with a minimal embedding request
|
// 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) {
|
} catch (error: any) {
|
||||||
// Check for specific AWS errors
|
// Check for specific AWS errors
|
||||||
if (error.name === "UnrecognizedClientException") {
|
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
|
* 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
|
* 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 {
|
try {
|
||||||
// Delegate validation to the OpenAI-compatible embedder
|
// Delegate validation to the OpenAI-compatible embedder
|
||||||
// The error messages will be specific to Gemini since we're using Gemini's base URL
|
// 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
|
* 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
|
* 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 {
|
try {
|
||||||
// Delegate validation to the OpenAI-compatible embedder
|
// Delegate validation to the OpenAI-compatible embedder
|
||||||
// The error messages will be specific to Mistral since we're using Mistral's base URL
|
// 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
|
* 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
|
* 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(
|
return withValidationErrorHandling(
|
||||||
async () => {
|
async () => {
|
||||||
// First check if Ollama service is running by trying to list models
|
// 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",
|
"ollama",
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -357,10 +357,11 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the OpenAI-compatible embedder configuration by testing endpoint connectivity and API key
|
* 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
|
* 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 () => {
|
return withValidationErrorHandling(async () => {
|
||||||
try {
|
try {
|
||||||
// Test with a minimal embedding request
|
// 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) {
|
} catch (error) {
|
||||||
// Capture telemetry for validation errors
|
// Capture telemetry for validation errors
|
||||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
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
|
* Validates the OpenAI embedder configuration by attempting a minimal embedding request.
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||||
return withValidationErrorHandling(async () => {
|
return withValidationErrorHandling(async () => {
|
||||||
try {
|
try {
|
||||||
// Test with a minimal embedding request
|
// 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) {
|
} catch (error) {
|
||||||
// Capture telemetry for validation errors
|
// Capture telemetry for validation errors
|
||||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
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
|
* Validates the OpenRouter embedder configuration by testing API connectivity.
|
||||||
* @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
|
||||||
*/
|
*/
|
||||||
async validateConfiguration(): Promise<{ valid: boolean; error?: string }> {
|
async validateConfiguration(): Promise<{ valid: boolean; error?: string; detectedDimension?: number }> {
|
||||||
return withValidationErrorHandling(async () => {
|
return withValidationErrorHandling(async () => {
|
||||||
try {
|
try {
|
||||||
// Test with a minimal embedding request
|
// 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) {
|
} catch (error) {
|
||||||
// Capture telemetry for validation errors
|
// Capture telemetry for validation errors
|
||||||
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
|
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
|
* 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
|
* 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 {
|
try {
|
||||||
// Delegate validation to the OpenAI-compatible embedder
|
// 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
|
// 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.
|
* 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
|
get embedderInfo(): EmbedderInfo
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -390,15 +390,10 @@ export class CodeIndexManager {
|
||||||
const rooIgnoreController = new RooIgnoreController(workspacePath)
|
const rooIgnoreController = new RooIgnoreController(workspacePath)
|
||||||
await rooIgnoreController.initialize()
|
await rooIgnoreController.initialize()
|
||||||
|
|
||||||
// (Re)Create shared service instances
|
// Create embedder first to validate and detect embedding dimension
|
||||||
const { embedder, vectorStore, scanner, fileWatcher } = this._serviceFactory.createServices(
|
const embedder = this._serviceFactory.createEmbedder()
|
||||||
this.context,
|
|
||||||
this._cacheManager!,
|
|
||||||
ignoreInstance,
|
|
||||||
rooIgnoreController,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Validate embedder configuration before proceeding
|
// Validate embedder configuration and detect actual embedding dimension
|
||||||
const validationResult = await this._serviceFactory.validateEmbedder(embedder)
|
const validationResult = await this._serviceFactory.validateEmbedder(embedder)
|
||||||
if (!validationResult.valid) {
|
if (!validationResult.valid) {
|
||||||
const errorMessage = validationResult.error || "Embedder configuration validation failed"
|
const errorMessage = validationResult.error || "Embedder configuration validation failed"
|
||||||
|
|
@ -406,6 +401,20 @@ export class CodeIndexManager {
|
||||||
throw new Error(errorMessage)
|
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
|
// (Re)Initialize orchestrator
|
||||||
this._orchestrator = new CodeIndexOrchestrator(
|
this._orchestrator = new CodeIndexOrchestrator(
|
||||||
this._configManager!,
|
this._configManager!,
|
||||||
|
|
|
||||||
|
|
@ -112,10 +112,13 @@ export class CodeIndexServiceFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates an embedder instance to ensure it's properly configured.
|
* 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
|
* @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 {
|
try {
|
||||||
return await embedder.validateConfiguration()
|
return await embedder.validateConfiguration()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -136,8 +139,10 @@ export class CodeIndexServiceFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a vector store instance using the current configuration.
|
* 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 config = this.configManager.getConfig()
|
||||||
|
|
||||||
const provider = config.embedderProvider as EmbedderProvider
|
const provider = config.embedderProvider as EmbedderProvider
|
||||||
|
|
@ -147,12 +152,20 @@ export class CodeIndexServiceFactory {
|
||||||
|
|
||||||
let vectorSize: number | undefined
|
let vectorSize: number | undefined
|
||||||
|
|
||||||
// First try to get the model-specific dimension from profiles
|
// Priority order for vector dimension:
|
||||||
vectorSize = getModelDimension(provider, modelId)
|
// 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
|
// Only use manual dimension if model doesn't have a built-in dimension
|
||||||
if (!vectorSize && config.modelDimension && config.modelDimension > 0) {
|
if (!vectorSize && config.modelDimension && config.modelDimension > 0) {
|
||||||
vectorSize = config.modelDimension
|
vectorSize = config.modelDimension
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (vectorSize === undefined || vectorSize <= 0) {
|
if (vectorSize === undefined || vectorSize <= 0) {
|
||||||
|
|
@ -230,6 +243,11 @@ export class CodeIndexServiceFactory {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates all required service dependencies if the service is properly configured.
|
* 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
|
* @throws Error if the service is not properly configured
|
||||||
*/
|
*/
|
||||||
public createServices(
|
public createServices(
|
||||||
|
|
@ -237,6 +255,7 @@ export class CodeIndexServiceFactory {
|
||||||
cacheManager: CacheManager,
|
cacheManager: CacheManager,
|
||||||
ignoreInstance: Ignore,
|
ignoreInstance: Ignore,
|
||||||
rooIgnoreController?: RooIgnoreController,
|
rooIgnoreController?: RooIgnoreController,
|
||||||
|
detectedDimension?: number,
|
||||||
): {
|
): {
|
||||||
embedder: IEmbedder
|
embedder: IEmbedder
|
||||||
vectorStore: IVectorStore
|
vectorStore: IVectorStore
|
||||||
|
|
@ -249,7 +268,7 @@ export class CodeIndexServiceFactory {
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedder = this.createEmbedder()
|
const embedder = this.createEmbedder()
|
||||||
const vectorStore = this.createVectorStore()
|
const vectorStore = this.createVectorStore(detectedDimension)
|
||||||
const parser = codeParser
|
const parser = codeParser
|
||||||
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
|
const scanner = this.createDirectoryScanner(embedder, vectorStore, parser, ignoreInstance)
|
||||||
const fileWatcher = this.createFileWatcher(
|
const fileWatcher = this.createFileWatcher(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue