fix(components): bound follow-up prompt generation latency#6600
fix(components): bound follow-up prompt generation latency#6600auberginewly wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a timeout and retry mechanism (invokeWithTimeoutAndRetry) for generating follow-up prompts across multiple LLM providers, along with comprehensive unit tests. The review feedback suggests improving this mechanism by integrating an AbortController to pass an AbortSignal to the LLM .invoke() calls, which prevents resource leaks from dangling requests. Additionally, it is recommended to log errors on the final retry attempt instead of swallowing them, and to refactor the internal helper function to use positional parameters instead of an options object.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| type FollowUpPromptTask<T> = () => Promise<T> | ||
|
|
||
| const DEFAULT_FOLLOW_UP_PROMPT_TIMEOUT_MS = 10000 | ||
| const DEFAULT_FOLLOW_UP_PROMPT_MAX_RETRIES = 1 | ||
|
|
||
| class FollowUpPromptTimeoutError extends Error { | ||
| constructor(timeoutMs: number) { | ||
| super(`Follow-up prompt generation timed out after ${timeoutMs}ms`) | ||
| this.name = 'FollowUpPromptTimeoutError' | ||
| } | ||
| } | ||
|
|
||
| const createTimeoutPromise = <T>(timeoutMs: number): { promise: Promise<T>; clear: () => void } => { | ||
| let timeout: NodeJS.Timeout | ||
| const promise = new Promise<T>((_, reject) => { | ||
| timeout = setTimeout(() => reject(new FollowUpPromptTimeoutError(timeoutMs)), timeoutMs) | ||
| }) | ||
|
|
||
| return { | ||
| promise, | ||
| clear: () => clearTimeout(timeout) | ||
| } | ||
| } | ||
|
|
||
| const invokeWithTimeoutAndRetry = async <T>( | ||
| task: FollowUpPromptTask<T>, | ||
| options: FollowUpPromptGenerationOptions = {} | ||
| ): Promise<T | undefined> => { | ||
| const timeoutMs = options.timeoutMs ?? DEFAULT_FOLLOW_UP_PROMPT_TIMEOUT_MS | ||
| const maxRetries = options.maxRetries ?? DEFAULT_FOLLOW_UP_PROMPT_MAX_RETRIES | ||
|
|
||
| for (let attempt = 0; attempt <= maxRetries; attempt += 1) { | ||
| const timeout = createTimeoutPromise<T>(timeoutMs) | ||
|
|
||
| try { | ||
| const taskPromise = task() | ||
| taskPromise.catch(() => undefined) | ||
|
|
||
| return await Promise.race([taskPromise, timeout.promise]) | ||
| } catch { | ||
| if (attempt === maxRetries) return undefined | ||
| } finally { | ||
| timeout.clear() | ||
| } | ||
| } | ||
|
|
||
| return undefined | ||
| } |
There was a problem hiding this comment.
Issues Identified:
- Resource Leak / Dangling Requests: When a timeout occurs, the in-flight LLM request is left running in the background, consuming server resources, connections, and potentially API tokens/billing. We should use an
AbortControllerand pass itsAbortSignalto the task so that the in-flight request can be aborted when a timeout or failure occurs. - Swallowed Errors: The catch block completely swallows all errors without any logging, making it extremely difficult to debug failures in follow-up prompt generation. We should log the error on the final attempt.
- Internal Function Parameters: For internal functions like
invokeWithTimeoutAndRetry, we should avoid using an options object and instead use positional parameters until the parameter list becomes complex or more parameters are needed.
Solution:
Update FollowUpPromptTask to accept an AbortSignal, and use AbortController to abort the in-flight request on timeout or failure. Also, log the error on the final attempt, and use positional parameters for invokeWithTimeoutAndRetry instead of an options object.
type FollowUpPromptTask<T> = (signal: AbortSignal) => Promise<T>
const DEFAULT_FOLLOW_UP_PROMPT_TIMEOUT_MS = 10000
const DEFAULT_FOLLOW_UP_PROMPT_MAX_RETRIES = 1
class FollowUpPromptTimeoutError extends Error {
constructor(timeoutMs: number) {
super(`Follow-up prompt generation timed out after ${timeoutMs}ms`)
this.name = 'FollowUpPromptTimeoutError'
}
}
const createTimeoutPromise = <T>(timeoutMs: number): { promise: Promise<T>; clear: () => void } => {
let timeout: NodeJS.Timeout
const promise = new Promise<T>((_, reject) => {
timeout = setTimeout(() => reject(new FollowUpPromptTimeoutError(timeoutMs)), timeoutMs)
})
return {
promise,
clear: () => clearTimeout(timeout)
}
}
const invokeWithTimeoutAndRetry = async <T>(
task: FollowUpPromptTask<T>,
timeoutMs: number = DEFAULT_FOLLOW_UP_PROMPT_TIMEOUT_MS,
maxRetries: number = DEFAULT_FOLLOW_UP_PROMPT_MAX_RETRIES
): Promise<T | undefined> => {
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
const controller = new AbortController()
const timeout = createTimeoutPromise<T>(timeoutMs)
try {
const taskPromise = task(controller.signal)
taskPromise.catch(() => undefined)
return await Promise.race([taskPromise, timeout.promise])
} catch (error) {
controller.abort()
if (attempt === maxRetries) {
console.error(`Follow-up prompt generation failed after ${maxRetries + 1} attempts:`, error)
return undefined
}
} finally {
timeout.clear()
}
}
return undefined
}References
- For internal functions, defer refactoring to an options object until the parameter list becomes complex or more parameters are needed.
| return await invokeWithTimeoutAndRetry(async () => { | ||
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | ||
| return structuredResponse as FollowUpPromptResult | ||
| }) |
There was a problem hiding this comment.
Pass the AbortSignal to the Anthropic .invoke() call to ensure the request is aborted on timeout or failure.
| return await invokeWithTimeoutAndRetry(async () => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | |
| return structuredResponse as FollowUpPromptResult | |
| }) | |
| return await invokeWithTimeoutAndRetry(async (signal) => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt, { signal }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) |
| return await invokeWithTimeoutAndRetry(async () => { | ||
| const structuredResponse = await chain.invoke({ | ||
| history: apiMessageContent, | ||
| format_instructions: formatInstructions | ||
| }) | ||
| return structuredResponse as FollowUpPromptResult | ||
| }) |
There was a problem hiding this comment.
Pass the AbortSignal to the Azure OpenAI .invoke() call to ensure the request is aborted on timeout or failure.
| return await invokeWithTimeoutAndRetry(async () => { | |
| const structuredResponse = await chain.invoke({ | |
| history: apiMessageContent, | |
| format_instructions: formatInstructions | |
| }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) | |
| return await invokeWithTimeoutAndRetry(async (signal) => { | |
| const structuredResponse = await chain.invoke({ | |
| history: apiMessageContent, | |
| format_instructions: formatInstructions | |
| }, { signal }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) |
| return await invokeWithTimeoutAndRetry(async () => { | ||
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | ||
| return structuredResponse as FollowUpPromptResult | ||
| }) |
There was a problem hiding this comment.
Pass the AbortSignal to the Google GenAI .invoke() call to ensure the request is aborted on timeout or failure.
| return await invokeWithTimeoutAndRetry(async () => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | |
| return structuredResponse as FollowUpPromptResult | |
| }) | |
| return await invokeWithTimeoutAndRetry(async (signal) => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt, { signal }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) |
| return await invokeWithTimeoutAndRetry(async () => { | ||
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | ||
| return structuredResponse as FollowUpPromptResult | ||
| }) |
There was a problem hiding this comment.
Pass the AbortSignal to the Mistral .invoke() call to ensure the request is aborted on timeout or failure.
| return await invokeWithTimeoutAndRetry(async () => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | |
| return structuredResponse as FollowUpPromptResult | |
| }) | |
| return await invokeWithTimeoutAndRetry(async (signal) => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt, { signal }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) |
| return await invokeWithTimeoutAndRetry(async () => { | ||
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | ||
| return structuredResponse as FollowUpPromptResult | ||
| }) |
There was a problem hiding this comment.
Pass the AbortSignal to the OpenAI .invoke() call to ensure the request is aborted on timeout or failure.
| return await invokeWithTimeoutAndRetry(async () => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | |
| return structuredResponse as FollowUpPromptResult | |
| }) | |
| return await invokeWithTimeoutAndRetry(async (signal) => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt, { signal }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) |
| return await invokeWithTimeoutAndRetry(async () => { | ||
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | ||
| return structuredResponse as FollowUpPromptResult | ||
| }) |
There was a problem hiding this comment.
Pass the AbortSignal to the Groq .invoke() call to ensure the request is aborted on timeout or failure.
| return await invokeWithTimeoutAndRetry(async () => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt) | |
| return structuredResponse as FollowUpPromptResult | |
| }) | |
| return await invokeWithTimeoutAndRetry(async (signal) => { | |
| const structuredResponse = await structuredLLM.invoke(followUpPromptsPrompt, { signal }) | |
| return structuredResponse as FollowUpPromptResult | |
| }) |
Problem
Follow-up prompt generation is an optional step, but provider calls are currently awaited directly in the main prediction flow. If a provider request hangs, fails slowly, or returns invalid output, the main response path can be delayed or interrupted even though follow-up prompts are not required for the chat response itself.
Solution
This PR makes follow-up prompt generation best-effort by wrapping provider calls with a shared timeout and retry helper. The helper uses a default 10s timeout and one retry, then gracefully returns
undefinedif generation still fails. This keeps the existing public API unchanged while bounding latency for the optional follow-up prompt step.Changes
generateFollowUpPrompts(...)function signature.Testing
Test coverage includes:
undefinedinstead of blocking indefinitely.Notes for Reviewer
generateFollowUpPrompts.