Harden audio transcription fetch path with retries and timeout

This commit is contained in:
William Valentin
2026-02-22 19:54:58 -08:00
parent abaa9be3f1
commit 487f26e36d
6 changed files with 175 additions and 4 deletions
+48 -1
View File
@@ -25,6 +25,53 @@ const PROVIDER_ENDPOINTS: Record<string, string> = {
llamacpp: 'http://localhost:8080/v1/audio/transcriptions',
};
const TRANSCRIPTION_FETCH_MAX_ATTEMPTS = 3;
const TRANSCRIPTION_FETCH_TIMEOUT_MS = 45_000;
const TRANSCRIPTION_FETCH_BASE_DELAY_MS = 250;
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function isTransientNetworkError(error: unknown): boolean {
const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
return message.includes('fetch failed')
|| message.includes('network')
|| message.includes('timeout')
|| message.includes('timed out')
|| message.includes('econnrefused')
|| message.includes('econnreset')
|| message.includes('enotfound')
|| message.includes('ehostunreach');
}
async function fetchWithRetry(endpoint: string, init: RequestInit): Promise<Response> {
for (let attempt = 1; attempt <= TRANSCRIPTION_FETCH_MAX_ATTEMPTS; attempt += 1) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), TRANSCRIPTION_FETCH_TIMEOUT_MS);
try {
return await fetch(endpoint, { ...init, signal: controller.signal });
} catch (error) {
const timedOut = error instanceof Error && error.name === 'AbortError';
const normalizedMessage = timedOut
? `request timed out after ${TRANSCRIPTION_FETCH_TIMEOUT_MS}ms`
: (error instanceof Error ? error.message : String(error));
const retriable = timedOut || isTransientNetworkError(error);
const exhausted = attempt >= TRANSCRIPTION_FETCH_MAX_ATTEMPTS;
if (!retriable || exhausted) {
throw new Error(
`Transcription request to ${endpoint} failed after ${attempt} attempt(s): ${normalizedMessage}`,
);
}
await sleep(TRANSCRIPTION_FETCH_BASE_DELAY_MS * (2 ** (attempt - 1)));
} finally {
clearTimeout(timeout);
}
}
throw new Error(`Transcription request to ${endpoint} failed after retries`);
}
function validateUrl(url: string): { valid: boolean; error?: string } {
let parsed: URL;
try {
@@ -387,7 +434,7 @@ export function createAudioTranscribeTool(audioConfig?: AudioTranscriptionConfig
fetchOptions.headers = headers;
}
const response = await fetch(endpoint, fetchOptions);
const response = await fetchWithRetry(endpoint, fetchOptions);
if (!response.ok) {
const errorText = await response.text();