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
This commit is contained in:
Roo 2025-07-15 12:24:49 +00:00
parent 8a3dcfb593
commit 7647fd168f
10 changed files with 175 additions and 9 deletions

View file

@ -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<typeof codebaseIndexConfigSchema>
@ -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<typeof codebaseIndexProviderSchema>

View file

@ -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)

View file

@ -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 = {

View file

@ -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

View file

@ -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",

View file

@ -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

View file

@ -247,6 +247,10 @@ export interface WebviewMessage {
codebaseIndexSearchMaxResults?: number
codebaseIndexSearchMinScore?: number
// Ollama timeout settings
codebaseIndexOllamaEmbeddingTimeoutMs?: number
codebaseIndexOllamaValidationTimeoutMs?: number
// Secret settings
codeIndexOpenAiKey?: string
codeIndexQdrantApiKey?: string

View file

@ -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<CodeIndexPopoverProps> = ({
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<CodeIndexPopoverProps> = ({
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<CodeIndexPopoverProps> = ({
</p>
)}
</div>
{/* Ollama Timeout Settings */}
<div className="space-y-4 mt-4 p-3 border border-vscode-dropdown-border rounded">
<h5 className="text-sm font-medium mb-2">
{t("settings:codeIndex.ollamaTimeoutSettings")}
</h5>
<div className="space-y-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.ollamaEmbeddingTimeoutLabel")}
</label>
<StandardTooltip
content={t(
"settings:codeIndex.ollamaEmbeddingTimeoutDescription",
)}>
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
</StandardTooltip>
</div>
<VSCodeTextField
value={
currentSettings.codebaseIndexOllamaEmbeddingTimeoutMs?.toString() ||
"30000"
}
onInput={(e: any) => {
const value = parseInt(e.target.value) || 30000
updateSetting(
"codebaseIndexOllamaEmbeddingTimeoutMs",
Math.max(1000, Math.min(300000, value)),
)
}}
placeholder="30000"
className="w-full"
/>
<p className="text-xs text-vscode-descriptionForeground">
{t("settings:codeIndex.ollamaEmbeddingTimeoutHelp")}
</p>
</div>
<div className="space-y-2">
<div className="flex items-center gap-2">
<label className="text-sm font-medium">
{t("settings:codeIndex.ollamaValidationTimeoutLabel")}
</label>
<StandardTooltip
content={t(
"settings:codeIndex.ollamaValidationTimeoutDescription",
)}>
<span className="codicon codicon-info text-xs text-vscode-descriptionForeground cursor-help" />
</StandardTooltip>
</div>
<VSCodeTextField
value={
currentSettings.codebaseIndexOllamaValidationTimeoutMs?.toString() ||
"10000"
}
onInput={(e: any) => {
const value = parseInt(e.target.value) || 10000
updateSetting(
"codebaseIndexOllamaValidationTimeoutMs",
Math.max(1000, Math.min(60000, value)),
)
}}
placeholder="10000"
className="w-full"
/>
<p className="text-xs text-vscode-descriptionForeground">
{t("settings:codeIndex.ollamaValidationTimeoutHelp")}
</p>
</div>
</div>
</>
)}

View file

@ -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,

View file

@ -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.",