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")}
+
+
+
+
+
+ {t("settings:codeIndex.ollamaEmbeddingTimeoutLabel")}
+
+
+
+
+
+
{
+ 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")}
+
+
+
+
+
+
+ {t("settings:codeIndex.ollamaValidationTimeoutLabel")}
+
+
+
+
+
+
{
+ 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.",