From 6ab3705123c678b38f7defcba595d6a6b837adb7 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Fri, 19 Dec 2025 12:16:33 +0545 Subject: [PATCH] Add timeout and retry logic to GroqTranscriptionService --- .../GroqTranscriptionService.swift | 70 ++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/VoiceInk/Services/CloudTranscription/GroqTranscriptionService.swift b/VoiceInk/Services/CloudTranscription/GroqTranscriptionService.swift index 27b5932..daf3da5 100644 --- a/VoiceInk/Services/CloudTranscription/GroqTranscriptionService.swift +++ b/VoiceInk/Services/CloudTranscription/GroqTranscriptionService.swift @@ -3,8 +3,15 @@ import os class GroqTranscriptionService { private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "GroqService") - + private let baseTimeout: TimeInterval = 120 + private let maxRetries: Int = 2 + private let initialRetryDelay: TimeInterval = 1.0 + func transcribe(audioURL: URL, model: any TranscriptionModel) async throws -> String { + return try await transcribeWithRetry(audioURL: audioURL, model: model) + } + + private func makeTranscriptionRequest(audioURL: URL, model: any TranscriptionModel) async throws -> String { let config = try getAPIConfig(for: model) let boundary = "Boundary-\(UUID().uuidString)" @@ -12,6 +19,7 @@ class GroqTranscriptionService { request.httpMethod = "POST" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(config.apiKey)", forHTTPHeaderField: "Authorization") + request.timeoutInterval = baseTimeout let body = try createOpenAICompatibleRequestBody(audioURL: audioURL, modelName: config.modelName, boundary: boundary) @@ -34,7 +42,65 @@ class GroqTranscriptionService { throw CloudTranscriptionError.noTranscriptionReturned } } - + + private func transcribeWithRetry(audioURL: URL, model: any TranscriptionModel) async throws -> String { + var retries = 0 + var currentDelay = initialRetryDelay + + while retries < self.maxRetries { + do { + return try await makeTranscriptionRequest(audioURL: audioURL, model: model) + } catch let error as CloudTranscriptionError { + switch error { + case .networkError: + retries += 1 + if retries < self.maxRetries { + logger.warning("Transcription request failed, retrying in \(currentDelay)s... (Attempt \(retries)/\(self.maxRetries))") + try await Task.sleep(nanoseconds: UInt64(currentDelay * 1_000_000_000)) + currentDelay *= 2 + } else { + logger.error("Transcription request failed after \(self.maxRetries) retries.") + throw error + } + case .apiRequestFailed(let statusCode, _): + if (500...599).contains(statusCode) || statusCode == 429 { + retries += 1 + if retries < self.maxRetries { + logger.warning("Transcription request failed with status \(statusCode), retrying in \(currentDelay)s... (Attempt \(retries)/\(self.maxRetries))") + try await Task.sleep(nanoseconds: UInt64(currentDelay * 1_000_000_000)) + currentDelay *= 2 + } else { + logger.error("Transcription request failed after \(self.maxRetries) retries.") + throw error + } + } else { + throw error + } + default: + throw error + } + } catch { + let nsError = error as NSError + if nsError.domain == NSURLErrorDomain && + [NSURLErrorNotConnectedToInternet, NSURLErrorTimedOut, NSURLErrorNetworkConnectionLost].contains(nsError.code) { + retries += 1 + if retries < self.maxRetries { + logger.warning("Transcription request failed with network error, retrying in \(currentDelay)s... (Attempt \(retries)/\(self.maxRetries))") + try await Task.sleep(nanoseconds: UInt64(currentDelay * 1_000_000_000)) + currentDelay *= 2 + } else { + logger.error("Transcription request failed after \(self.maxRetries) retries with network error.") + throw CloudTranscriptionError.networkError(error) + } + } else { + throw error + } + } + } + + throw CloudTranscriptionError.noTranscriptionReturned + } + private func getAPIConfig(for model: any TranscriptionModel) throws -> APIConfig { guard let apiKey = UserDefaults.standard.string(forKey: "GROQAPIKey"), !apiKey.isEmpty else { throw CloudTranscriptionError.missingAPIKey