@@ -11,6 +11,11 @@ import { TelemetryEventName } from "@roo-code/types"
1111const OLLAMA_EMBEDDING_TIMEOUT_MS = 60000 // 60 seconds for embedding requests
1212const OLLAMA_VALIDATION_TIMEOUT_MS = 30000 // 30 seconds for validation requests
1313
14+ // Retry configuration
15+ const MAX_RETRIES = 3
16+ const INITIAL_RETRY_DELAY_MS = 1000 // 1 second
17+ const MAX_RETRY_DELAY_MS = 10000 // 10 seconds
18+
1419/**
1520 * Implements the IEmbedder interface using a local Ollama instance.
1621 */
@@ -64,77 +69,154 @@ export class CodeIndexOllamaEmbedder implements IEmbedder {
6469 } )
6570 : texts
6671
67- try {
68- // Note: Standard Ollama API uses 'prompt' for single text, not 'input' for array.
69- // Implementing based on user's specific request structure.
72+ let lastError : Error | null = null
73+ let retryDelay = INITIAL_RETRY_DELAY_MS
7074
71- // Add timeout to prevent indefinite hanging
72- const controller = new AbortController ( )
73- const timeoutId = setTimeout ( ( ) => controller . abort ( ) , OLLAMA_EMBEDDING_TIMEOUT_MS )
75+ for ( let attempt = 1 ; attempt <= MAX_RETRIES ; attempt ++ ) {
76+ try {
77+ // Note: Standard Ollama API uses 'prompt' for single text, not 'input' for array.
78+ // Implementing based on user's specific request structure.
7479
75- const response = await fetch ( url , {
76- method : "POST" ,
77- headers : {
78- "Content-Type" : "application/json" ,
79- } ,
80- body : JSON . stringify ( {
81- model : modelToUse ,
82- input : processedTexts , // Using 'input' as requested
83- } ) ,
84- signal : controller . signal ,
85- } )
86- clearTimeout ( timeoutId )
87-
88- if ( ! response . ok ) {
89- let errorBody = t ( "embeddings:ollama.couldNotReadErrorBody" )
90- try {
91- errorBody = await response . text ( )
92- } catch ( e ) {
93- // Ignore error reading body
94- }
95- throw new Error (
96- t ( "embeddings:ollama.requestFailed" , {
97- status : response . status ,
98- statusText : response . statusText ,
99- errorBody,
80+ // Add timeout to prevent indefinite hanging
81+ const controller = new AbortController ( )
82+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , OLLAMA_EMBEDDING_TIMEOUT_MS )
83+
84+ const response = await fetch ( url , {
85+ method : "POST" ,
86+ headers : {
87+ "Content-Type" : "application/json" ,
88+ } ,
89+ body : JSON . stringify ( {
90+ model : modelToUse ,
91+ input : processedTexts , // Using 'input' as requested
10092 } ) ,
101- )
102- }
93+ signal : controller . signal ,
94+ } )
95+ clearTimeout ( timeoutId )
10396
104- const data = await response . json ( )
97+ if ( ! response . ok ) {
98+ let errorBody = t ( "embeddings:ollama.couldNotReadErrorBody" )
99+ let errorDetails : any = { }
100+ try {
101+ errorBody = await response . text ( )
102+ // Try to parse as JSON to get more details
103+ try {
104+ errorDetails = JSON . parse ( errorBody )
105+ } catch {
106+ // Not JSON, use as is
107+ }
108+ } catch ( e ) {
109+ // Ignore error reading body
110+ }
105111
106- // Extract embeddings using 'embeddings' key as requested
107- const embeddings = data . embeddings
108- if ( ! embeddings || ! Array . isArray ( embeddings ) ) {
109- throw new Error ( t ( "embeddings:ollama.invalidResponseStructure" ) )
110- }
112+ // Check if it's a model not found error
113+ if (
114+ response . status === 404 ||
115+ errorDetails . error ?. includes ( "model" ) ||
116+ errorDetails . error ?. includes ( "not found" )
117+ ) {
118+ throw new Error ( t ( "embeddings:ollama.modelNotFound" , { modelId : modelToUse } ) )
119+ }
111120
112- return {
113- embeddings : embeddings ,
114- }
115- } catch ( error : any ) {
116- // Capture telemetry before reformatting the error
117- TelemetryService . instance . captureEvent ( TelemetryEventName . CODE_INDEX_ERROR , {
118- error : sanitizeErrorMessage ( error instanceof Error ? error . message : String ( error ) ) ,
119- stack : error instanceof Error ? sanitizeErrorMessage ( error . stack || "" ) : undefined ,
120- location : "OllamaEmbedder:createEmbeddings" ,
121- } )
122-
123- // Log the original error for debugging purposes
124- console . error ( "Ollama embedding failed:" , error )
125-
126- // Handle specific error types with better messages
127- if ( error . name === "AbortError" ) {
128- throw new Error ( t ( "embeddings:validation.connectionFailed" ) )
129- } else if ( error . message ?. includes ( "fetch failed" ) || error . code === "ECONNREFUSED" ) {
130- throw new Error ( t ( "embeddings:ollama.serviceNotRunning" , { baseUrl : this . baseUrl } ) )
131- } else if ( error . code === "ENOTFOUND" ) {
132- throw new Error ( t ( "embeddings:ollama.hostNotFound" , { baseUrl : this . baseUrl } ) )
121+ throw new Error (
122+ t ( "embeddings:ollama.requestFailed" , {
123+ status : response . status ,
124+ statusText : response . statusText ,
125+ errorBody,
126+ } ) ,
127+ )
128+ }
129+
130+ const data = await response . json ( )
131+
132+ // Log the response structure for debugging
133+ if ( ! data || typeof data !== "object" ) {
134+ console . error ( "Ollama API response is not an object:" , data )
135+ throw new Error ( t ( "embeddings:ollama.invalidResponseStructure" ) )
136+ }
137+
138+ // Extract embeddings using 'embeddings' key as requested
139+ const embeddings = data . embeddings
140+ if ( ! embeddings || ! Array . isArray ( embeddings ) ) {
141+ console . error ( "Ollama API response structure:" , JSON . stringify ( data , null , 2 ) )
142+ throw new Error ( t ( "embeddings:ollama.invalidResponseStructure" ) )
143+ }
144+
145+ // Validate that embeddings is an array of arrays (2D array)
146+ if ( embeddings . length > 0 && ! Array . isArray ( embeddings [ 0 ] ) ) {
147+ console . error (
148+ "Ollama embeddings format invalid - expected array of arrays, got:" ,
149+ typeof embeddings [ 0 ] ,
150+ )
151+ throw new Error ( t ( "embeddings:ollama.invalidResponseStructure" ) )
152+ }
153+
154+ return {
155+ embeddings : embeddings ,
156+ }
157+ } catch ( error : any ) {
158+ lastError = error
159+
160+ // Don't retry for certain errors
161+ if (
162+ error . message ?. includes ( t ( "embeddings:ollama.modelNotFound" , { modelId : "" } ) . split ( ":" ) [ 0 ] ) ||
163+ error . message ?. includes ( t ( "embeddings:ollama.invalidResponseStructure" ) )
164+ ) {
165+ break
166+ }
167+
168+ // Check if we should retry
169+ if ( attempt < MAX_RETRIES ) {
170+ // Check if it's a transient error that we should retry
171+ if (
172+ error . name === "AbortError" ||
173+ error . message ?. includes ( "fetch failed" ) ||
174+ error . code === "ECONNREFUSED" ||
175+ error . code === "ENOTFOUND" ||
176+ error . code === "ETIMEDOUT" ||
177+ error . code === "ECONNRESET"
178+ ) {
179+ console . log ( `Ollama embedding attempt ${ attempt } failed, retrying in ${ retryDelay } ms...` )
180+ await new Promise ( ( resolve ) => setTimeout ( resolve , retryDelay ) )
181+ retryDelay = Math . min ( retryDelay * 2 , MAX_RETRY_DELAY_MS ) // Exponential backoff
182+ continue
183+ }
184+ }
185+
186+ // If we're here, we're not retrying
187+ break
133188 }
189+ }
190+
191+ // If we get here, all retries failed
192+ if ( ! lastError ) {
193+ lastError = new Error ( "Unknown error in Ollama embedder" )
194+ }
195+ // Capture telemetry before reformatting the error
196+ TelemetryService . instance . captureEvent ( TelemetryEventName . CODE_INDEX_ERROR , {
197+ error : sanitizeErrorMessage ( lastError instanceof Error ? lastError . message : String ( lastError ) ) ,
198+ stack : lastError instanceof Error ? sanitizeErrorMessage ( lastError . stack || "" ) : undefined ,
199+ location : "OllamaEmbedder:createEmbeddings" ,
200+ } )
201+
202+ // Log the original error for debugging purposes
203+ console . error ( "Ollama embedding failed after all retries:" , lastError )
134204
135- // Re-throw a more specific error for the caller
136- throw new Error ( t ( "embeddings:ollama.embeddingFailed" , { message : error . message } ) )
205+ // Handle specific error types with better messages
206+ if ( lastError . name === "AbortError" ) {
207+ throw new Error ( t ( "embeddings:validation.connectionFailed" ) )
208+ } else if ( lastError . message ?. includes ( "fetch failed" ) || ( lastError as any ) . code === "ECONNREFUSED" ) {
209+ throw new Error ( t ( "embeddings:ollama.serviceNotRunning" , { baseUrl : this . baseUrl } ) )
210+ } else if ( ( lastError as any ) . code === "ENOTFOUND" ) {
211+ throw new Error ( t ( "embeddings:ollama.hostNotFound" , { baseUrl : this . baseUrl } ) )
212+ } else if ( lastError . message ?. includes ( t ( "embeddings:ollama.modelNotFound" , { modelId : "" } ) . split ( ":" ) [ 0 ] ) ) {
213+ throw lastError // Re-throw model not found as is
214+ } else if ( lastError . message ?. includes ( t ( "embeddings:ollama.invalidResponseStructure" ) ) ) {
215+ throw lastError // Re-throw invalid response structure as is
137216 }
217+
218+ // Re-throw a more specific error for the caller
219+ throw new Error ( t ( "embeddings:ollama.embeddingFailed" , { message : lastError . message } ) )
138220 }
139221
140222 /**
0 commit comments