Skip to content
Draft
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
5 changes: 4 additions & 1 deletion packages/types/src/embedding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ export type EmbedderProvider =
| "bedrock"
| "openrouter" // Add other providers as needed.

export type EmbeddingPurpose = "index" | "query"

export interface EmbeddingModelProfile {
dimension: number
scoreThreshold?: number // Model-specific minimum score threshold for semantic search.
queryPrefix?: string // Optional prefix required by the model for queries.
queryPrefix?: string // Optional prefix required by the model for search queries.
documentPrefix?: string // Optional prefix required by the model for indexing/document embedding.
// Add other model-specific properties if needed, e.g., context window size.
}

Expand Down
4 changes: 2 additions & 2 deletions src/services/code-index/embedders/__tests__/gemini.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe("GeminiEmbedder", () => {
const result = await embedder.createEmbeddings(texts)

// Assert
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "gemini-embedding-001")
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "gemini-embedding-001", undefined)
expect(result).toEqual(mockResponse)
})

Expand All @@ -141,7 +141,7 @@ describe("GeminiEmbedder", () => {
const result = await embedder.createEmbeddings(texts, "gemini-embedding-001")

// Assert
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "gemini-embedding-001")
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "gemini-embedding-001", undefined)
expect(result).toEqual(mockResponse)
})

Expand Down
4 changes: 2 additions & 2 deletions src/services/code-index/embedders/__tests__/mistral.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe("MistralEmbedder", () => {
const result = await embedder.createEmbeddings(texts)

// Assert
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "codestral-embed-2505")
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "codestral-embed-2505", undefined)
expect(result).toEqual(mockResponse)
})

Expand All @@ -124,7 +124,7 @@ describe("MistralEmbedder", () => {
const result = await embedder.createEmbeddings(texts, "codestral-embed-2505")

// Assert
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "codestral-embed-2505")
expect(mockCreateEmbeddings).toHaveBeenCalledWith(texts, "codestral-embed-2505", undefined)
expect(result).toEqual(mockResponse)
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("VercelAiGatewayEmbedder", () => {
expect(mockOpenAICompatibleEmbedder.createEmbeddings).toHaveBeenCalledWith(
texts,
"openai/text-embedding-3-large",
undefined,
)
expect(result).toBe(expectedResponse)
})
Expand All @@ -110,7 +111,7 @@ describe("VercelAiGatewayEmbedder", () => {
const result = await embedder.createEmbeddings(texts, customModel)

// Assert
expect(mockOpenAICompatibleEmbedder.createEmbeddings).toHaveBeenCalledWith(texts, customModel)
expect(mockOpenAICompatibleEmbedder.createEmbeddings).toHaveBeenCalledWith(texts, customModel, undefined)
expect(result).toBe(expectedResponse)
})

Expand All @@ -125,6 +126,7 @@ describe("VercelAiGatewayEmbedder", () => {
expect(mockOpenAICompatibleEmbedder.createEmbeddings).toHaveBeenCalledWith(
texts,
"openai/text-embedding-3-large",
undefined,
)
})
})
Expand Down
3 changes: 2 additions & 1 deletion src/services/code-index/embedders/bedrock.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { BedrockRuntimeClient, InvokeModelCommand, InvokeModelCommandInput } from "@aws-sdk/client-bedrock-runtime"
import { fromIni, fromNodeProviderChain } from "@aws-sdk/credential-providers"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces"
Expand Down Expand Up @@ -55,7 +56,7 @@ export class BedrockEmbedder implements IEmbedder {
* @param model Optional model identifier
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, _purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
const modelToUse = model || this.defaultModelId

const allEmbeddings: number[][] = []
Expand Down
5 changes: 3 additions & 2 deletions src/services/code-index/embedders/gemini.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { OpenAICompatibleEmbedder } from "./openai-compatible"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import { GEMINI_MAX_ITEM_TOKENS } from "../constants"
Expand Down Expand Up @@ -68,11 +69,11 @@ export class GeminiEmbedder implements IEmbedder {
* @param model Optional model identifier (uses constructor model if not provided)
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
try {
// Use the provided model or fall back to the instance's model
const modelToUse = model || this.modelId
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse)
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse, purpose)
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
Expand Down
5 changes: 3 additions & 2 deletions src/services/code-index/embedders/mistral.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { OpenAICompatibleEmbedder } from "./openai-compatible"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import { MAX_ITEM_TOKENS } from "../constants"
Expand Down Expand Up @@ -46,11 +47,11 @@ export class MistralEmbedder implements IEmbedder {
* @param model Optional model identifier (uses constructor model if not provided)
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
try {
// Use the provided model or fall back to the instance's model
const modelToUse = model || this.modelId
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse)
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse, purpose)
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
Expand Down
9 changes: 5 additions & 4 deletions src/services/code-index/embedders/ollama.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { ApiHandlerOptions } from "../../../shared/api"
import { EmbedderInfo, EmbeddingResponse, IEmbedder } from "../interfaces"
import { getModelQueryPrefix } from "../../../shared/embeddingModels"
import { getModelPrefixForPurpose } from "../../../shared/embeddingModels"
import { MAX_ITEM_TOKENS } from "../constants"
import { t } from "../../../i18n"
import { withValidationErrorHandling, sanitizeErrorMessage } from "../shared/validation-helpers"
Expand Down Expand Up @@ -35,12 +36,12 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
* @param model - Optional model ID to override the default.
* @returns A promise that resolves to an EmbeddingResponse containing the embeddings and usage data.
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
const modelToUse = model || this.defaultModelId
const url = `${this.baseUrl}/api/embed` // Endpoint as specified

// Apply model-specific query prefix if required
const queryPrefix = getModelQueryPrefix("ollama", modelToUse)
// Apply model-specific prefix based on purpose (query vs index)
const queryPrefix = getModelPrefixForPurpose("ollama", modelToUse, purpose)
const processedTexts = queryPrefix
? texts.map((text, index) => {
// Prevent double-prefixing
Expand Down
9 changes: 5 additions & 4 deletions src/services/code-index/embedders/openai-compatible.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { OpenAI } from "openai"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import {
Expand All @@ -6,7 +7,7 @@ import {
MAX_BATCH_RETRIES as MAX_RETRIES,
INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS,
} from "../constants"
import { getDefaultModelId, getModelQueryPrefix } from "../../../shared/embeddingModels"
import { getDefaultModelId, getModelPrefixForPurpose } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, HttpError, formatEmbeddingError } from "../shared/validation-helpers"
import { TelemetryEventName } from "@roo-code/types"
Expand Down Expand Up @@ -91,11 +92,11 @@ export class OpenAICompatibleEmbedder implements IEmbedder {
* @param model Optional model identifier
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
const modelToUse = model || this.defaultModelId

// Apply model-specific query prefix if required
const queryPrefix = getModelQueryPrefix("openai-compatible", modelToUse)
// Apply model-specific prefix based on purpose (query vs index)
const queryPrefix = getModelPrefixForPurpose("openai-compatible", modelToUse, purpose)
const processedTexts = queryPrefix
? texts.map((text, index) => {
// Prevent double-prefixing
Expand Down
9 changes: 5 additions & 4 deletions src/services/code-index/embedders/openai.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { OpenAI } from "openai"
import { OpenAiNativeHandler } from "../../../api/providers/openai-native"
import { ApiHandlerOptions } from "../../../shared/api"
Expand All @@ -8,7 +9,7 @@ import {
MAX_BATCH_RETRIES as MAX_RETRIES,
INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS,
} from "../constants"
import { getModelQueryPrefix } from "../../../shared/embeddingModels"
import { getModelPrefixForPurpose } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, formatEmbeddingError, HttpError } from "../shared/validation-helpers"
import { TelemetryEventName } from "@roo-code/types"
Expand Down Expand Up @@ -47,11 +48,11 @@ export class OpenAiEmbedder extends OpenAiNativeHandler implements IEmbedder {
* @param model Optional model identifier
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
const modelToUse = model || this.defaultModelId

// Apply model-specific query prefix if required
const queryPrefix = getModelQueryPrefix("openai", modelToUse)
// Apply model-specific prefix based on purpose (query vs index)
const queryPrefix = getModelPrefixForPurpose("openai", modelToUse, purpose)
const processedTexts = queryPrefix
? texts.map((text, index) => {
// Prevent double-prefixing
Expand Down
9 changes: 5 additions & 4 deletions src/services/code-index/embedders/openrouter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { OpenAI } from "openai"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import {
Expand All @@ -6,7 +7,7 @@ import {
MAX_BATCH_RETRIES as MAX_RETRIES,
INITIAL_RETRY_DELAY_MS as INITIAL_DELAY_MS,
} from "../constants"
import { getDefaultModelId, getModelQueryPrefix } from "../../../shared/embeddingModels"
import { getDefaultModelId, getModelPrefixForPurpose } from "../../../shared/embeddingModels"
import { t } from "../../../i18n"
import { withValidationErrorHandling, HttpError, formatEmbeddingError } from "../shared/validation-helpers"
import { TelemetryEventName } from "@roo-code/types"
Expand Down Expand Up @@ -95,11 +96,11 @@ export class OpenRouterEmbedder implements IEmbedder {
* @param model Optional model identifier
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
const modelToUse = model || this.defaultModelId

// Apply model-specific query prefix if required
const queryPrefix = getModelQueryPrefix("openrouter", modelToUse)
// Apply model-specific prefix based on purpose (query vs index)
const queryPrefix = getModelPrefixForPurpose("openrouter", modelToUse, purpose)
const processedTexts = queryPrefix
? texts.map((text, index) => {
// Prevent double-prefixing
Expand Down
5 changes: 3 additions & 2 deletions src/services/code-index/embedders/vercel-ai-gateway.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { EmbeddingPurpose } from "@roo-code/types"
import { OpenAICompatibleEmbedder } from "./openai-compatible"
import { IEmbedder, EmbeddingResponse, EmbedderInfo } from "../interfaces/embedder"
import { MAX_ITEM_TOKENS } from "../constants"
Expand Down Expand Up @@ -55,11 +56,11 @@ export class VercelAiGatewayEmbedder implements IEmbedder {
* @param model Optional model identifier (uses constructor model if not provided)
* @returns Promise resolving to embedding response
*/
async createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse> {
async createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse> {
try {
// Use the provided model or fall back to the instance's model
const modelToUse = model || this.modelId
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse)
return await this.openAICompatibleEmbedder.createEmbeddings(texts, modelToUse, purpose)
} catch (error) {
TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, {
error: error instanceof Error ? error.message : String(error),
Expand Down
7 changes: 6 additions & 1 deletion src/services/code-index/interfaces/embedder.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { EmbeddingPurpose } from "@roo-code/types"

/**
* Interface for code index embedders.
* This interface is implemented by both OpenAI and Ollama embedders.
Expand All @@ -7,9 +9,12 @@ export interface IEmbedder {
* Creates embeddings for the given texts.
* @param texts Array of text strings to create embeddings for
* @param model Optional model ID to use for embeddings
* @param purpose Optional embedding purpose ("index" for document indexing, "query" for search queries).
* When "index", the model's documentPrefix is applied (if any).
* When "query" or undefined, the model's queryPrefix is applied (if any) for backward compatibility.
* @returns Promise resolving to an EmbeddingResponse
*/
createEmbeddings(texts: string[], model?: string): Promise<EmbeddingResponse>
createEmbeddings(texts: string[], model?: string, purpose?: EmbeddingPurpose): Promise<EmbeddingResponse>

/**
* Validates the embedder configuration by testing connectivity and credentials.
Expand Down
2 changes: 1 addition & 1 deletion src/services/code-index/processors/file-watcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -566,7 +566,7 @@ export class FileWatcher implements IFileWatcher {
let pointsToUpsert: PointStruct[] = []
if (this.embedder && blocks.length > 0) {
const texts = blocks.map((block) => block.content)
const { embeddings } = await this.embedder.createEmbeddings(texts)
const { embeddings } = await this.embedder.createEmbeddings(texts, undefined, "index")

pointsToUpsert = blocks.map((block, index) => {
const normalizedAbsolutePath = generateNormalizedAbsolutePath(block.file_path, this.workspacePath)
Expand Down
4 changes: 2 additions & 2 deletions src/services/code-index/processors/scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,8 +442,8 @@ export class DirectoryScanner implements IDirectoryScanner {
}
// --- End Deletion Step ---

// Create embeddings for batch
const { embeddings } = await this.embedder.createEmbeddings(batchTexts)
// Create embeddings for batch (purpose: "index" for document indexing)
const { embeddings } = await this.embedder.createEmbeddings(batchTexts, undefined, "index")

// Prepare points for Qdrant
const points = batchBlocks.map((block, index) => {
Expand Down
4 changes: 2 additions & 2 deletions src/services/code-index/search-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ export class CodeIndexSearchService {
}

try {
// Generate embedding for query
const embeddingResponse = await this.embedder.createEmbeddings([query])
// Generate embedding for query (purpose: "query" for search queries)
const embeddingResponse = await this.embedder.createEmbeddings([query], undefined, "query")
const vector = embeddingResponse?.embeddings[0]
if (!vector) {
throw new Error("Failed to generate embedding for query.")
Expand Down
74 changes: 74 additions & 0 deletions src/shared/__tests__/embeddingModels.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import { describe, it, expect } from "vitest"
import {
getModelDimension,
getModelScoreThreshold,
getModelQueryPrefix,
getModelDocumentPrefix,
getModelPrefixForPurpose,
getDefaultModelId,
EMBEDDING_MODEL_PROFILES,
} from "../embeddingModels"
Expand Down Expand Up @@ -92,4 +95,75 @@ describe("embeddingModels", () => {
expect(defaultModel).toBe("codestral-embed-2505")
})
})

describe("getModelQueryPrefix", () => {
it("should return queryPrefix for nomic-embed-code on ollama", () => {
const prefix = getModelQueryPrefix("ollama", "nomic-embed-code")
expect(prefix).toBe("Represent this query for searching relevant code: ")
})

it("should return queryPrefix for nomic-embed-code on openai-compatible", () => {
const prefix = getModelQueryPrefix("openai-compatible", "nomic-embed-code")
expect(prefix).toBe("Represent this query for searching relevant code: ")
})

it("should return undefined for models without queryPrefix", () => {
const prefix = getModelQueryPrefix("openai", "text-embedding-3-small")
expect(prefix).toBeUndefined()
})

it("should return undefined for unknown model", () => {
const prefix = getModelQueryPrefix("ollama", "unknown-model")
expect(prefix).toBeUndefined()
})

it("should return undefined for unknown provider", () => {
const prefix = getModelQueryPrefix("unknown-provider" as any, "some-model")
expect(prefix).toBeUndefined()
})
})

describe("getModelDocumentPrefix", () => {
it("should return undefined for nomic-embed-code on ollama (no prefix for indexing)", () => {
const prefix = getModelDocumentPrefix("ollama", "nomic-embed-code")
expect(prefix).toBeUndefined()
})

it("should return undefined for nomic-embed-code on openai-compatible (no prefix for indexing)", () => {
const prefix = getModelDocumentPrefix("openai-compatible", "nomic-embed-code")
expect(prefix).toBeUndefined()
})

it("should return undefined for models without documentPrefix", () => {
const prefix = getModelDocumentPrefix("openai", "text-embedding-3-small")
expect(prefix).toBeUndefined()
})

it("should return undefined for unknown provider", () => {
const prefix = getModelDocumentPrefix("unknown-provider" as any, "some-model")
expect(prefix).toBeUndefined()
})
})

describe("getModelPrefixForPurpose", () => {
it("should return queryPrefix when purpose is 'query'", () => {
const prefix = getModelPrefixForPurpose("ollama", "nomic-embed-code", "query")
expect(prefix).toBe("Represent this query for searching relevant code: ")
})

it("should return documentPrefix (undefined) when purpose is 'index'", () => {
const prefix = getModelPrefixForPurpose("ollama", "nomic-embed-code", "index")
expect(prefix).toBeUndefined()
})

it("should fall back to queryPrefix when purpose is undefined (backward compatibility)", () => {
const prefix = getModelPrefixForPurpose("ollama", "nomic-embed-code", undefined)
expect(prefix).toBe("Represent this query for searching relevant code: ")
})

it("should return undefined for models without any prefix regardless of purpose", () => {
expect(getModelPrefixForPurpose("openai", "text-embedding-3-small", "query")).toBeUndefined()
expect(getModelPrefixForPurpose("openai", "text-embedding-3-small", "index")).toBeUndefined()
})
})
})
Loading
Loading