Add localhost->127.0.0.1 fallback for transcription fetch
This commit is contained in:
@@ -460,6 +460,28 @@ describe('transcribeAudio', () => {
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('falls back from localhost to 127.0.0.1 on transient fetch failures', async () => {
|
||||
vi.mocked(global.fetch)
|
||||
.mockRejectedValueOnce(new TypeError('fetch failed'))
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({ text: mockTranscript }),
|
||||
} as Response);
|
||||
|
||||
const config: AudioTranscriptionConfig = {
|
||||
endpoint: 'http://localhost:18801/v1/audio/transcriptions',
|
||||
apiKey: 'test-key',
|
||||
model: 'test-model',
|
||||
};
|
||||
|
||||
const result = await transcribeAudio(oggAudioAttachment, config);
|
||||
|
||||
expect(result).toBe(mockTranscript);
|
||||
expect(global.fetch).toHaveBeenCalledTimes(2);
|
||||
expect(vi.mocked(global.fetch).mock.calls[0]?.[0]).toBe('http://localhost:18801/v1/audio/transcriptions');
|
||||
expect(vi.mocked(global.fetch).mock.calls[1]?.[0]).toBe('http://127.0.0.1:18801/v1/audio/transcriptions');
|
||||
});
|
||||
|
||||
// Positive: uses Whisper-1 model by default.
|
||||
it('uses whisper-1 model by default', async () => {
|
||||
const config: AudioTranscriptionConfig = {
|
||||
|
||||
+25
-3
@@ -44,22 +44,42 @@ function isTransientNetworkError(error: unknown): boolean {
|
||||
|| message.includes('ehostunreach');
|
||||
}
|
||||
|
||||
function buildEndpointCandidates(endpoint: string): string[] {
|
||||
try {
|
||||
const parsed = new URL(endpoint);
|
||||
if (parsed.hostname !== 'localhost') {
|
||||
return [endpoint];
|
||||
}
|
||||
const ipv4Endpoint = new URL(endpoint);
|
||||
ipv4Endpoint.hostname = '127.0.0.1';
|
||||
return [endpoint, ipv4Endpoint.toString()];
|
||||
} catch {
|
||||
return [endpoint];
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchTranscriptionWithRetry(endpoint: string, init: RequestInit): Promise<Response> {
|
||||
const endpointCandidates = buildEndpointCandidates(endpoint);
|
||||
let lastErrorMessage = 'Unknown network error';
|
||||
let lastEndpoint = endpoint;
|
||||
for (let attempt = 1; attempt <= TRANSCRIPTION_FETCH_MAX_ATTEMPTS; attempt += 1) {
|
||||
const endpointForAttempt = endpointCandidates[(attempt - 1) % endpointCandidates.length];
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), TRANSCRIPTION_FETCH_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(endpoint, { ...init, signal: controller.signal });
|
||||
return await fetch(endpointForAttempt, { ...init, signal: controller.signal });
|
||||
} catch (error) {
|
||||
const timedOut = error instanceof Error && error.name === 'AbortError';
|
||||
const retriable = timedOut || isTransientNetworkError(error);
|
||||
const normalizedMessage = timedOut
|
||||
? `request timed out after ${TRANSCRIPTION_FETCH_TIMEOUT_MS}ms`
|
||||
: (error instanceof Error ? error.message : String(error));
|
||||
lastErrorMessage = normalizedMessage;
|
||||
lastEndpoint = endpointForAttempt;
|
||||
const exhausted = attempt >= TRANSCRIPTION_FETCH_MAX_ATTEMPTS;
|
||||
if (!retriable || exhausted) {
|
||||
throw new Error(
|
||||
`Transcription request to ${endpoint} failed after ${attempt} attempt(s): ${normalizedMessage}`,
|
||||
`Transcription service connectivity failure at ${lastEndpoint} after ${attempt} attempt(s): ${normalizedMessage}. This indicates endpoint/network availability, not missing audio bytes.`,
|
||||
);
|
||||
}
|
||||
await sleep(TRANSCRIPTION_FETCH_BASE_DELAY_MS * (2 ** (attempt - 1)));
|
||||
@@ -68,7 +88,9 @@ async function fetchTranscriptionWithRetry(endpoint: string, init: RequestInit):
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Transcription request to ${endpoint} failed after retries`);
|
||||
throw new Error(
|
||||
`Transcription service connectivity failure at ${lastEndpoint} after retries: ${lastErrorMessage}. This indicates endpoint/network availability, not missing audio bytes.`,
|
||||
);
|
||||
}
|
||||
|
||||
/** Check whether an attachment is a supported image type. */
|
||||
|
||||
Reference in New Issue
Block a user