From 7647fd168f4894b1b836d3e7b29a4af25c6621f8 Mon Sep 17 00:00:00 2001 From: Roo Date: Tue, 15 Jul 2025 12:24:49 +0000 Subject: [PATCH] feat: add configurable timeout settings for Ollama embedder - Add timeout configuration options for Ollama embedding and validation operations - Default to 30s for embedding requests and 10s for validation requests - Add UI components in CodeIndexPopover for timeout configuration with validation - Update configuration interfaces and type definitions to support timeout settings - Integrate timeout settings into existing configuration management pipeline - Add comprehensive test coverage for timeout functionality - Add translation support for timeout settings Fixes #5733 --- packages/types/src/codebase-index.ts | 6 ++ src/core/webview/webviewMessageHandler.ts | 14 ++++ .../__tests__/service-factory.spec.ts | 26 ++++++ src/services/code-index/config-manager.ts | 13 ++- src/services/code-index/embedders/ollama.ts | 19 ++++- src/services/code-index/interfaces/config.ts | 10 ++- src/shared/WebviewMessage.ts | 4 + .../src/components/chat/CodeIndexPopover.tsx | 81 +++++++++++++++++++ .../src/context/ExtensionStateContext.tsx | 2 + webview-ui/src/i18n/locales/en/settings.json | 9 ++- 10 files changed, 175 insertions(+), 9 deletions(-) diff --git a/packages/types/src/codebase-index.ts b/packages/types/src/codebase-index.ts index 0ad19d8676a..0854cb594db 100644 --- a/packages/types/src/codebase-index.ts +++ b/packages/types/src/codebase-index.ts @@ -34,6 +34,9 @@ export const codebaseIndexConfigSchema = z.object({ // OpenAI Compatible specific fields codebaseIndexOpenAiCompatibleBaseUrl: z.string().optional(), codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), + // Ollama timeout settings for codebase indexing + codebaseIndexOllamaEmbeddingTimeoutMs: z.number().int().min(1000).max(300000).optional(), + codebaseIndexOllamaValidationTimeoutMs: z.number().int().min(1000).max(60000).optional(), }) export type CodebaseIndexConfig = z.infer @@ -62,6 +65,9 @@ export const codebaseIndexProviderSchema = z.object({ codebaseIndexOpenAiCompatibleApiKey: z.string().optional(), codebaseIndexOpenAiCompatibleModelDimension: z.number().optional(), codebaseIndexGeminiApiKey: z.string().optional(), + // Ollama timeout settings for codebase indexing + codebaseIndexOllamaEmbeddingTimeoutMs: z.number().int().min(1000).max(300000).optional(), + codebaseIndexOllamaValidationTimeoutMs: z.number().int().min(1000).max(60000).optional(), }) export type CodebaseIndexProvider = z.infer diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e70b39df8fd..034ffcfb1e6 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1966,6 +1966,20 @@ export const webviewMessageHandler = async ( codebaseIndexSearchMinScore: settings.codebaseIndexSearchMinScore, } + // Save Ollama timeout settings to global state + if (settings.codebaseIndexOllamaEmbeddingTimeoutMs !== undefined) { + await updateGlobalState( + "codebaseIndexOllamaEmbeddingTimeoutMs", + settings.codebaseIndexOllamaEmbeddingTimeoutMs, + ) + } + if (settings.codebaseIndexOllamaValidationTimeoutMs !== undefined) { + await updateGlobalState( + "codebaseIndexOllamaValidationTimeoutMs", + settings.codebaseIndexOllamaValidationTimeoutMs, + ) + } + // Save global state first await updateGlobalState("codebaseIndexConfig", globalStateConfig) diff --git a/src/services/code-index/__tests__/service-factory.spec.ts b/src/services/code-index/__tests__/service-factory.spec.ts index 1d8f7ba4786..9b2fd960d96 100644 --- a/src/services/code-index/__tests__/service-factory.spec.ts +++ b/src/services/code-index/__tests__/service-factory.spec.ts @@ -143,6 +143,32 @@ describe("CodeIndexServiceFactory", () => { }) }) + it("should pass timeout parameters to Ollama embedder when configured", () => { + // Arrange + const testModelId = "nomic-embed-text:latest" + const testConfig = { + embedderProvider: "ollama", + modelId: testModelId, + ollamaOptions: { + ollamaBaseUrl: "http://localhost:11434", + embeddingTimeoutMs: 45000, + validationTimeoutMs: 15000, + }, + } + mockConfigManager.getConfig.mockReturnValue(testConfig as any) + + // Act + factory.createEmbedder() + + // Assert + expect(MockedCodeIndexOllamaEmbedder).toHaveBeenCalledWith({ + ollamaBaseUrl: "http://localhost:11434", + ollamaModelId: testModelId, + embeddingTimeoutMs: 45000, + validationTimeoutMs: 15000, + }) + }) + it("should throw error when OpenAI API key is missing", () => { // Arrange const testConfig = { diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 9958f456c3e..d5c82963db1 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -1,7 +1,7 @@ import { ApiHandlerOptions } from "../../shared/api" import { ContextProxy } from "../../core/config/ContextProxy" import { EmbedderProvider } from "./interfaces/manager" -import { CodeIndexConfig, PreviousConfigSnapshot } from "./interfaces/config" +import { CodeIndexConfig, PreviousConfigSnapshot, OllamaConfigOptions } from "./interfaces/config" import { DEFAULT_SEARCH_MIN_SCORE, DEFAULT_MAX_SEARCH_RESULTS } from "./constants" import { getDefaultModelId, getModelDimension, getModelScoreThreshold } from "../../shared/embeddingModels" @@ -15,7 +15,7 @@ export class CodeIndexConfigManager { private modelId?: string private modelDimension?: number private openAiOptions?: ApiHandlerOptions - private ollamaOptions?: ApiHandlerOptions + private ollamaOptions?: OllamaConfigOptions private openAiCompatibleOptions?: { baseUrl: string; apiKey: string } private geminiOptions?: { apiKey: string } private qdrantUrl?: string = "http://localhost:6333" @@ -68,6 +68,10 @@ export class CodeIndexConfigManager { const openAiCompatibleApiKey = this.contextProxy?.getSecret("codebaseIndexOpenAiCompatibleApiKey") ?? "" const geminiApiKey = this.contextProxy?.getSecret("codebaseIndexGeminiApiKey") ?? "" + // Get Ollama timeout settings from global state + const ollamaEmbeddingTimeoutMs = this.contextProxy?.getGlobalState("codebaseIndexOllamaEmbeddingTimeoutMs") + const ollamaValidationTimeoutMs = this.contextProxy?.getGlobalState("codebaseIndexOllamaValidationTimeoutMs") + // Update instance variables with configuration this.codebaseIndexEnabled = codebaseIndexEnabled ?? true this.qdrantUrl = codebaseIndexQdrantUrl @@ -108,6 +112,9 @@ export class CodeIndexConfigManager { this.ollamaOptions = { ollamaBaseUrl: codebaseIndexEmbedderBaseUrl, + ollamaModelId: codebaseIndexEmbedderModelId, + embeddingTimeoutMs: ollamaEmbeddingTimeoutMs, + validationTimeoutMs: ollamaValidationTimeoutMs, } this.openAiCompatibleOptions = @@ -132,7 +139,7 @@ export class CodeIndexConfigManager { modelId?: string modelDimension?: number openAiOptions?: ApiHandlerOptions - ollamaOptions?: ApiHandlerOptions + ollamaOptions?: OllamaConfigOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } qdrantUrl?: string diff --git a/src/services/code-index/embedders/ollama.ts b/src/services/code-index/embedders/ollama.ts index 20b22b92bf0..0ab425b2e2f 100644 --- a/src/services/code-index/embedders/ollama.ts +++ b/src/services/code-index/embedders/ollama.ts @@ -6,6 +6,12 @@ import { t } from "../../../i18n" import { withValidationErrorHandling, sanitizeErrorMessage } from "../shared/validation-helpers" import { TelemetryService } from "@roo-code/telemetry" import { TelemetryEventName } from "@roo-code/types" +import { OllamaConfigOptions } from "../interfaces/config" + +export interface OllamaEmbedderOptions extends ApiHandlerOptions { + embeddingTimeoutMs?: number + validationTimeoutMs?: number +} /** * Implements the IEmbedder interface using a local Ollama instance. @@ -13,11 +19,16 @@ import { TelemetryEventName } from "@roo-code/types" export class CodeIndexOllamaEmbedder implements IEmbedder { private readonly baseUrl: string private readonly defaultModelId: string + private readonly embeddingTimeoutMs: number + private readonly validationTimeoutMs: number - constructor(options: ApiHandlerOptions) { + constructor(options: OllamaEmbedderOptions) { // Ensure ollamaBaseUrl and ollamaModelId exist on ApiHandlerOptions or add defaults this.baseUrl = options.ollamaBaseUrl || "http://localhost:11434" this.defaultModelId = options.ollamaModelId || "nomic-embed-text:latest" + // Default timeouts: 30s for embedding (3x original), 10s for validation (2x original) + this.embeddingTimeoutMs = options.embeddingTimeoutMs || 30000 + this.validationTimeoutMs = options.validationTimeoutMs || 10000 } /** @@ -61,7 +72,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { // Add timeout to prevent indefinite hanging const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout + const timeoutId = setTimeout(() => controller.abort(), this.embeddingTimeoutMs) const response = await fetch(url, { method: "POST", @@ -140,7 +151,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { // Add timeout to prevent indefinite hanging const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout + const timeoutId = setTimeout(() => controller.abort(), this.validationTimeoutMs) const modelsResponse = await fetch(modelsUrl, { method: "GET", @@ -197,7 +208,7 @@ export class CodeIndexOllamaEmbedder implements IEmbedder { // Add timeout for test request too const testController = new AbortController() - const testTimeoutId = setTimeout(() => testController.abort(), 5000) + const testTimeoutId = setTimeout(() => testController.abort(), this.validationTimeoutMs) const testResponse = await fetch(testUrl, { method: "POST", diff --git a/src/services/code-index/interfaces/config.ts b/src/services/code-index/interfaces/config.ts index 190a23e2a3e..f72a6f2ec5e 100644 --- a/src/services/code-index/interfaces/config.ts +++ b/src/services/code-index/interfaces/config.ts @@ -1,6 +1,14 @@ import { ApiHandlerOptions } from "../../../shared/api" // Adjust path if needed import { EmbedderProvider } from "./manager" +// Interface for Ollama-specific options including timeout configuration +export interface OllamaConfigOptions { + ollamaBaseUrl?: string + ollamaModelId?: string + embeddingTimeoutMs?: number + validationTimeoutMs?: number +} + /** * Configuration state for the code indexing feature */ @@ -10,7 +18,7 @@ export interface CodeIndexConfig { modelId?: string modelDimension?: number // Generic dimension property for all providers openAiOptions?: ApiHandlerOptions - ollamaOptions?: ApiHandlerOptions + ollamaOptions?: OllamaConfigOptions openAiCompatibleOptions?: { baseUrl: string; apiKey: string } geminiOptions?: { apiKey: string } qdrantUrl?: string diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index d5dc3f8c288..b8d9475656c 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -247,6 +247,10 @@ export interface WebviewMessage { codebaseIndexSearchMaxResults?: number codebaseIndexSearchMinScore?: number + // Ollama timeout settings + codebaseIndexOllamaEmbeddingTimeoutMs?: number + codebaseIndexOllamaValidationTimeoutMs?: number + // Secret settings codeIndexOpenAiKey?: string codeIndexQdrantApiKey?: string diff --git a/webview-ui/src/components/chat/CodeIndexPopover.tsx b/webview-ui/src/components/chat/CodeIndexPopover.tsx index b5742cc623a..cd342b6009e 100644 --- a/webview-ui/src/components/chat/CodeIndexPopover.tsx +++ b/webview-ui/src/components/chat/CodeIndexPopover.tsx @@ -62,6 +62,10 @@ interface LocalCodeIndexSettings { codebaseIndexSearchMaxResults?: number codebaseIndexSearchMinScore?: number + // Ollama timeout settings + codebaseIndexOllamaEmbeddingTimeoutMs?: number + codebaseIndexOllamaValidationTimeoutMs?: number + // Secret settings (start empty, will be loaded separately) codeIndexOpenAiKey?: string codeIndexQdrantApiKey?: string @@ -160,6 +164,8 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexEmbedderModelDimension: undefined, codebaseIndexSearchMaxResults: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS, codebaseIndexSearchMinScore: CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE, + codebaseIndexOllamaEmbeddingTimeoutMs: 30000, // 30 seconds default + codebaseIndexOllamaValidationTimeoutMs: 10000, // 10 seconds default codeIndexOpenAiKey: "", codeIndexQdrantApiKey: "", codebaseIndexOpenAiCompatibleBaseUrl: "", @@ -193,6 +199,10 @@ export const CodeIndexPopover: React.FC = ({ codebaseIndexConfig.codebaseIndexSearchMaxResults ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_RESULTS, codebaseIndexSearchMinScore: codebaseIndexConfig.codebaseIndexSearchMinScore ?? CODEBASE_INDEX_DEFAULTS.DEFAULT_SEARCH_MIN_SCORE, + codebaseIndexOllamaEmbeddingTimeoutMs: + codebaseIndexConfig.codebaseIndexOllamaEmbeddingTimeoutMs ?? 30000, + codebaseIndexOllamaValidationTimeoutMs: + codebaseIndexConfig.codebaseIndexOllamaValidationTimeoutMs ?? 10000, codeIndexOpenAiKey: "", codeIndexQdrantApiKey: "", codebaseIndexOpenAiCompatibleBaseUrl: codebaseIndexConfig.codebaseIndexOpenAiCompatibleBaseUrl || "", @@ -743,6 +753,77 @@ export const CodeIndexPopover: React.FC = ({

)} + + {/* Ollama Timeout Settings */} +
+
+ {t("settings:codeIndex.ollamaTimeoutSettings")} +
+ +
+
+ + + + +
+ { + const value = parseInt(e.target.value) || 30000 + updateSetting( + "codebaseIndexOllamaEmbeddingTimeoutMs", + Math.max(1000, Math.min(300000, value)), + ) + }} + placeholder="30000" + className="w-full" + /> +

+ {t("settings:codeIndex.ollamaEmbeddingTimeoutHelp")} +

+
+ +
+
+ + + + +
+ { + const value = parseInt(e.target.value) || 10000 + updateSetting( + "codebaseIndexOllamaValidationTimeoutMs", + Math.max(1000, Math.min(60000, value)), + ) + }} + placeholder="10000" + className="w-full" + /> +

+ {t("settings:codeIndex.ollamaValidationTimeoutHelp")} +

+
+
)} diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 6c70c8940d7..075258ccd19 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -223,6 +223,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode codebaseIndexEmbedderModelId: "", codebaseIndexSearchMaxResults: undefined, codebaseIndexSearchMinScore: undefined, + codebaseIndexOllamaEmbeddingTimeoutMs: 30000, + codebaseIndexOllamaValidationTimeoutMs: 10000, }, codebaseIndexModels: { ollama: {}, openai: {} }, alwaysAllowUpdateTodoList: true, diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 25428cfb16c..14bb8568126 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -119,7 +119,14 @@ "ollamaBaseUrlRequired": "Ollama base URL is required", "baseUrlRequired": "Base URL is required", "modelDimensionMinValue": "Model dimension must be greater than 0" - } + }, + "ollamaTimeoutSettings": "Ollama Timeout Settings", + "ollamaEmbeddingTimeoutLabel": "Embedding Timeout (ms)", + "ollamaEmbeddingTimeoutDescription": "Maximum time to wait for embedding requests to complete. Increase this value if you experience timeout errors with large code blocks.", + "ollamaEmbeddingTimeoutHelp": "Range: 1000-300000ms (1-300 seconds). Default: 30000ms (30 seconds)", + "ollamaValidationTimeoutLabel": "Validation Timeout (ms)", + "ollamaValidationTimeoutDescription": "Maximum time to wait for model validation requests to complete. This is used to test if the Ollama model is available.", + "ollamaValidationTimeoutHelp": "Range: 1000-60000ms (1-60 seconds). Default: 10000ms (10 seconds)" }, "autoApprove": { "description": "Allow Roo to automatically perform operations without requiring approval. Enable these settings only if you fully trust the AI and understand the associated security risks.",