Harden audio transcription fetch path with retries and timeout
This commit is contained in:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user