From 3085a2128e1f77d59329c67aeb78dca36957f28a Mon Sep 17 00:00:00 2001 From: Chaoqun Zhao Date: Mon, 23 Mar 2026 16:45:49 +0800 Subject: [PATCH] feat: add 2-second wall-clock timeout to Cloud Rewrite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use withThrowingTaskGroup to race the API call against a 2s deadline. If the Cerebras API doesn't respond within 2 seconds, the original transcription text is used immediately — no more 12-second waits. Implementation: - TaskGroup race: API call vs Task.sleep(2s), first result wins - Loser is cancelled via group.cancelAll() (frees URLSession resources) - URLRequest.timeoutInterval lowered from 15s to 3s as safety net - Timeout events logged for monitoring - rewriteTimeout defined as named Duration constant Co-Authored-By: Claude Opus 4.6 --- Sources/CloudRewriteService.swift | 35 +++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/Sources/CloudRewriteService.swift b/Sources/CloudRewriteService.swift index 849c7eb..c0ef29b 100644 --- a/Sources/CloudRewriteService.swift +++ b/Sources/CloudRewriteService.swift @@ -8,6 +8,10 @@ final class CloudRewriteService { private let apiKeyProvider: () -> String? private let endpoint: URL + /// Wall-clock timeout for the entire rewrite operation. + /// If the API doesn't respond within this duration, the original text is used. + private static let rewriteTimeout: Duration = .seconds(2) + /// Models to try in priority order. First available model wins. private static let preferredModels = [ "qwen-3-235b-a22b-instruct-2507", @@ -114,7 +118,31 @@ final class CloudRewriteService { } do { - return try await rewrite(text: text, apiKey: apiKey) + return try await withThrowingTaskGroup(of: String.self) { group in + // Race 1: the actual API call + group.addTask { [self] in + try await self.rewrite(text: text, apiKey: apiKey) + } + + // Race 2: timeout deadline + group.addTask { + try await Task.sleep(for: Self.rewriteTimeout) + throw RewriteTimeoutError() + } + + // First completed result wins; cancel the loser + guard let result = try await group.next() else { + return text + } + group.cancelAll() + return result + } + } catch is RewriteTimeoutError { + cloudRewriteLogger.warning("Cloud rewrite timed out (\(Self.rewriteTimeout, privacy: .public)), using original text (\(text.count, privacy: .public) chars)") + return text + } catch is CancellationError { + cloudRewriteLogger.debug("Cloud rewrite cancelled") + return text } catch let error as URLError { cloudRewriteLogger.warning("Cloud rewrite network error: \(error.code.rawValue, privacy: .public). Fallback to original text") return text @@ -132,7 +160,7 @@ final class CloudRewriteService { var request = URLRequest(url: endpoint) request.httpMethod = "POST" - request.timeoutInterval = 15 + request.timeoutInterval = 3 // Safety net; task group timeout (2s) is the primary enforcer request.setValue("application/json", forHTTPHeaderField: "Content-Type") request.setValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") @@ -265,3 +293,6 @@ private struct ModelListResponse: Decodable { let id: String } } + +/// Thrown by the timeout racer to signal that the rewrite deadline has been exceeded. +private struct RewriteTimeoutError: Error {}