Repository navigation
refactor(code-index): separate service factories and embedder validation #1818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| import { TelemetryService } from "@roo-code/telemetry" | ||
| import { TelemetryEventName } from "@roo-code/types" | ||
| import { t } from "../../../../i18n" | ||
| import embeddings from "../../../../i18n/locales/en/embeddings.json" | ||
| import { OpenAiEmbedder } from "../openai" | ||
| import { EmbedderValidationManager } from "../embedder-validation-manager" | ||
|
|
||
| vi.mock("../openai") | ||
| vi.mock("../../../../i18n", () => ({ t: vi.fn() })) | ||
| vi.mock("@roo-code/telemetry", () => ({ | ||
| TelemetryService: { instance: { captureEvent: vi.fn() } }, | ||
| })) | ||
|
|
||
| describe("EmbedderValidationManager", () => { | ||
| const manager = new EmbedderValidationManager() | ||
|
|
||
| beforeEach(() => vi.clearAllMocks()) | ||
|
|
||
| it.each([{ valid: true }, { valid: false, error: "Invalid credentials" }])( | ||
| "preserves the validation result %j without reporting an exception", | ||
| async (result) => { | ||
| const embedder = new OpenAiEmbedder({ openAiNativeApiKey: "test-key" }) | ||
| vi.mocked(embedder.validateConfiguration).mockResolvedValue(result) | ||
|
|
||
| expect(await manager.validateEmbedder(embedder)).toBe(result) | ||
| expect(embedder.validateConfiguration).toHaveBeenCalledExactlyOnceWith() | ||
| expect(TelemetryService.instance.captureEvent).not.toHaveBeenCalled() | ||
| }, | ||
| ) | ||
|
|
||
| it("preserves an exception's message and reports its stack", async () => { | ||
| const embedder = new OpenAiEmbedder({ openAiNativeApiKey: "test-key" }) | ||
| const error = new Error("Connection failed") | ||
| vi.mocked(embedder.validateConfiguration).mockRejectedValue(error) | ||
|
|
||
| expect(await manager.validateEmbedder(embedder)).toEqual({ valid: false, error: error.message }) | ||
| expect(t).not.toHaveBeenCalled() | ||
| expect(TelemetryService.instance.captureEvent).toHaveBeenCalledExactlyOnceWith( | ||
| TelemetryEventName.CODE_INDEX_ERROR, | ||
| { error: error.message, stack: error.stack, location: "validateEmbedder" }, | ||
| ) | ||
| }) | ||
|
|
||
| it.each(["unavailable", null, undefined])("handles non-Error rejection %s", async (error) => { | ||
| const embedder = new OpenAiEmbedder({ openAiNativeApiKey: "test-key" }) | ||
| vi.mocked(embedder.validateConfiguration).mockRejectedValue(error) | ||
| vi.mocked(t).mockReturnValue(embeddings.validation.configurationError) | ||
|
|
||
| expect(await manager.validateEmbedder(embedder)).toEqual({ | ||
| valid: false, | ||
| error: embeddings.validation.configurationError, | ||
| }) | ||
| expect(t).toHaveBeenCalledExactlyOnceWith("embeddings:validation.configurationError") | ||
| expect(TelemetryService.instance.captureEvent).toHaveBeenCalledExactlyOnceWith( | ||
| TelemetryEventName.CODE_INDEX_ERROR, | ||
| { error: String(error), stack: undefined, location: "validateEmbedder" }, | ||
| ) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| import { providerIdentifiers } from "@roo-code/types/provider-identifiers" | ||
|
|
||
| import { t } from "../../../i18n" | ||
| import type { CodeIndexConfig } from "../interfaces/config" | ||
| import type { IEmbedder } from "../interfaces" | ||
| import type { IEmbedderFactory } from "../interfaces/embedder-factory" | ||
| import { OpenAiEmbedderFactory } from "./factories/openai-embedder-factory" | ||
| import { OllamaEmbedderFactory } from "./factories/ollama-embedder-factory" | ||
| import { OpenAICompatibleEmbedderFactory } from "./factories/openai-compatible-embedder-factory" | ||
| import { GeminiEmbedderFactory } from "./factories/gemini-embedder-factory" | ||
| import { MistralEmbedderFactory } from "./factories/mistral-embedder-factory" | ||
| import { VercelAiGatewayEmbedderFactory } from "./factories/vercel-ai-gateway-embedder-factory" | ||
| import { BedrockEmbedderFactory } from "./factories/bedrock-embedder-factory" | ||
| import { OpenRouterEmbedderFactory } from "./factories/openrouter-embedder-factory" | ||
|
|
||
| type VectorEmbedderProvider = Exclude<CodeIndexConfig["embedderProvider"], "semble"> | ||
|
|
||
| export class EmbedderFactory implements IEmbedderFactory { | ||
| // Each provider owns its constructor arguments and validation. The record requires | ||
| // an entry for every vector embedder provider when the supported union changes. | ||
| // TODO: Inject the provider factory registry via DI instead of constructing factories here. | ||
| // https://github.com/Zoo-Code-Org/Zoo-Code/issues/1817 | ||
| private readonly embedderFactories: Readonly<Record<VectorEmbedderProvider, IEmbedderFactory>> = { | ||
| [providerIdentifiers.openai]: new OpenAiEmbedderFactory(), | ||
| [providerIdentifiers.ollama]: new OllamaEmbedderFactory(), | ||
| "openai-compatible": new OpenAICompatibleEmbedderFactory(), | ||
| [providerIdentifiers.gemini]: new GeminiEmbedderFactory(), | ||
| [providerIdentifiers.mistral]: new MistralEmbedderFactory(), | ||
| [providerIdentifiers.vercelAiGateway]: new VercelAiGatewayEmbedderFactory(), | ||
| [providerIdentifiers.bedrock]: new BedrockEmbedderFactory(), | ||
| [providerIdentifiers.openrouter]: new OpenRouterEmbedderFactory(), | ||
| } | ||
|
|
||
| public create(config: CodeIndexConfig): IEmbedder { | ||
| const provider = config.embedderProvider | ||
|
|
||
| // Semble is a self-contained search binary, not a vector embedder. | ||
| if (provider === "semble") { | ||
| throw new Error( | ||
| "Semble provider handles its own embedding. Do not call createEmbedder() for semble — use SembleProvider instead.", | ||
| ) | ||
| } | ||
|
|
||
| // Keep a runtime guard for invalid persisted configuration, including prototype keys. | ||
| if (!Object.prototype.hasOwnProperty.call(this.embedderFactories, provider)) { | ||
| throw new Error(t("embeddings:serviceFactory.invalidEmbedderType", { embedderProvider: provider })) | ||
|
Check warning on line 46 in src/services/code-index/embedders/embedder-factory.ts
|
||
| } | ||
|
|
||
| return this.embedderFactories[provider].create(config) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import { TelemetryService } from "@roo-code/telemetry" | ||
| import { TelemetryEventName } from "@roo-code/types" | ||
| import { t } from "../../../i18n" | ||
| import type { IEmbedder } from "../interfaces" | ||
|
|
||
| export class EmbedderValidationManager { | ||
| public async validateEmbedder(embedder: IEmbedder): Promise<{ valid: boolean; error?: string }> { | ||
| try { | ||
| return await embedder.validateConfiguration() | ||
| } catch (error) { | ||
| TelemetryService.instance.captureEvent(TelemetryEventName.CODE_INDEX_ERROR, { | ||
| error: error instanceof Error ? error.message : String(error), | ||
| stack: error instanceof Error ? error.stack : undefined, | ||
| location: "validateEmbedder", | ||
| }) | ||
|
|
||
| return { | ||
| valid: false, | ||
| error: error instanceof Error ? error.message : t("embeddings:validation.configurationError"), | ||
| } | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { BedrockEmbedder } from "../bedrock" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class BedrockEmbedderFactory implements IEmbedderFactory { | ||
| create({ bedrockOptions, modelId }: CodeIndexConfig): BedrockEmbedder { | ||
| return new BedrockEmbedder( | ||
| requireSetting(bedrockOptions?.region, "embeddings:serviceFactory.bedrockConfigMissing"), | ||
| bedrockOptions?.profile, | ||
|
Check warning on line 10 in src/services/code-index/embedders/factories/bedrock-embedder-factory.ts
|
||
| modelId, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { GeminiEmbedder } from "../gemini" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class GeminiEmbedderFactory implements IEmbedderFactory { | ||
| create({ geminiOptions, modelId }: CodeIndexConfig): GeminiEmbedder { | ||
| return new GeminiEmbedder( | ||
| requireSetting(geminiOptions?.apiKey, "embeddings:serviceFactory.geminiConfigMissing"), | ||
| modelId, | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { MistralEmbedder } from "../mistral" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class MistralEmbedderFactory implements IEmbedderFactory { | ||
| create({ mistralOptions, modelId }: CodeIndexConfig): MistralEmbedder { | ||
| return new MistralEmbedder( | ||
| requireSetting(mistralOptions?.apiKey, "embeddings:serviceFactory.mistralConfigMissing"), | ||
| modelId, | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { CodeIndexOllamaEmbedder } from "../ollama" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class OllamaEmbedderFactory implements IEmbedderFactory { | ||
| create({ ollamaOptions, modelId }: CodeIndexConfig): CodeIndexOllamaEmbedder { | ||
| return new CodeIndexOllamaEmbedder({ | ||
| ...ollamaOptions, | ||
| ollamaBaseUrl: requireSetting( | ||
| ollamaOptions?.ollamaBaseUrl, | ||
| "embeddings:serviceFactory.ollamaConfigMissing", | ||
| ), | ||
| ollamaModelId: modelId, | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { OpenAICompatibleEmbedder } from "../openai-compatible" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class OpenAICompatibleEmbedderFactory implements IEmbedderFactory { | ||
| create({ openAiCompatibleOptions, modelId }: CodeIndexConfig): OpenAICompatibleEmbedder { | ||
| return new OpenAICompatibleEmbedder( | ||
| requireSetting(openAiCompatibleOptions?.baseUrl, "embeddings:validation.baseUrlRequired"), | ||
| requireSetting(openAiCompatibleOptions?.apiKey, "embeddings:validation.apiKeyRequired"), | ||
|
Check warning on line 10 in src/services/code-index/embedders/factories/openai-compatible-embedder-factory.ts
|
||
| modelId, | ||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { OpenAiEmbedder } from "../openai" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class OpenAiEmbedderFactory implements IEmbedderFactory { | ||
| create({ openAiOptions, modelId }: CodeIndexConfig): OpenAiEmbedder { | ||
| return new OpenAiEmbedder({ | ||
| ...openAiOptions, | ||
| openAiNativeApiKey: requireSetting( | ||
| openAiOptions?.openAiNativeApiKey, | ||
| "embeddings:serviceFactory.openAiConfigMissing", | ||
| ), | ||
| openAiEmbeddingModelId: modelId, | ||
| }) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { OpenRouterEmbedder } from "../openrouter" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class OpenRouterEmbedderFactory implements IEmbedderFactory { | ||
| create({ openRouterOptions, modelId }: CodeIndexConfig): OpenRouterEmbedder { | ||
| return new OpenRouterEmbedder( | ||
| requireSetting(openRouterOptions?.apiKey, "embeddings:serviceFactory.openRouterConfigMissing"), | ||
| modelId, | ||
| undefined, // maxItemTokens | ||
| openRouterOptions?.specificProvider, | ||
|
Check warning on line 12 in src/services/code-index/embedders/factories/openrouter-embedder-factory.ts
|
||
| ) | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| import { t } from "../../../../i18n" | ||
|
|
||
| export function requireSetting(value: string | undefined, errorKey: string): string { | ||
| if (!value) { | ||
| throw new Error(t(errorKey)) | ||
| } | ||
| return value | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { CodeIndexConfig } from "../../interfaces/config" | ||
| import type { IEmbedderFactory } from "../../interfaces/embedder-factory" | ||
| import { VercelAiGatewayEmbedder } from "../vercel-ai-gateway" | ||
| import { requireSetting } from "./require-setting" | ||
|
|
||
| export class VercelAiGatewayEmbedderFactory implements IEmbedderFactory { | ||
| create({ vercelAiGatewayOptions, modelId }: CodeIndexConfig): VercelAiGatewayEmbedder { | ||
| return new VercelAiGatewayEmbedder( | ||
| requireSetting(vercelAiGatewayOptions?.apiKey, "embeddings:serviceFactory.vercelAiGatewayConfigMissing"), | ||
| modelId, | ||
| ) | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import type { Ignore } from "ignore" | ||
| import type { CacheManager } from "../cache-manager" | ||
| import type { ICodeParser, IDirectoryScanner, IEmbedder, IVectorStore } from "./index" | ||
|
|
||
| export interface DirectoryScannerFactoryOptions { | ||
| embedder: IEmbedder | ||
| vectorStore: IVectorStore | ||
| parser: ICodeParser | ||
| cacheManager: CacheManager | ||
| ignoreInstance: Ignore | ||
| } | ||
|
|
||
| export interface IDirectoryScannerFactory { | ||
| create(options: DirectoryScannerFactoryOptions): IDirectoryScanner | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| import type { CodeIndexConfig } from "./config" | ||
| import type { IEmbedder } from "./index" | ||
|
|
||
| /** Creates a vector embedder, validating the provider's required configuration. */ | ||
| export interface IEmbedderFactory { | ||
| create(config: CodeIndexConfig): IEmbedder | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import type * as vscode from "vscode" | ||
| import type { Ignore } from "ignore" | ||
| import type { RooIgnoreController } from "../../../core/ignore/RooIgnoreController" | ||
| import type { CacheManager } from "../cache-manager" | ||
| import type { IEmbedder, IFileWatcher, IVectorStore } from "./index" | ||
|
|
||
| export interface FileWatcherFactoryOptions { | ||
| workspacePath: string | ||
| context: vscode.ExtensionContext | ||
| embedder: IEmbedder | ||
| vectorStore: IVectorStore | ||
| cacheManager: CacheManager | ||
| ignoreInstance: Ignore | ||
| rooIgnoreController?: RooIgnoreController | ||
| } | ||
|
|
||
| export interface IFileWatcherFactory { | ||
| create(options: FileWatcherFactoryOptions): IFileWatcher | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import type { CodeIndexConfig } from "./config" | ||
| import type { IVectorStore } from "./index" | ||
|
|
||
| export interface IVectorStoreFactory { | ||
| create(config: CodeIndexConfig, workspacePath: string): IVectorStore | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.