From 63fac7d836cc1ce4e2bfa5c792e5aaaf58222740 Mon Sep 17 00:00:00 2001 From: Shreeraman A K <16458670+shreeraman96@users.noreply.github.com> Date: Thu, 23 Jul 2026 22:45:29 -0700 Subject: [PATCH 1/3] fix: skip cache-clear recovery on rate-limit and network errors (#683) Clearing the provider cache and re-preparing on a transient 429 or network failure forces a full re-download, which burns more of HuggingFace's fixed per-IP API window and turns one rate limit into a self-reinforcing failure loop. Classify rate-limit/HTML/network errors (walking the underlying-error chain) and rethrow them instead, leaving cached files resumable. --- Sources/Fluid/Services/ASRService.swift | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/Sources/Fluid/Services/ASRService.swift b/Sources/Fluid/Services/ASRService.swift index 8c3855c1..385eaf14 100644 --- a/Sources/Fluid/Services/ASRService.swift +++ b/Sources/Fluid/Services/ASRService.swift @@ -3169,6 +3169,14 @@ final class ASRService: ObservableObject { if Task.isCancelled || Self.isModelPreparationCancellation(error) { throw CancellationError() } + if Self.isRateLimitOrNetworkError(error) { + DebugLogger.shared.warning( + "ASRService: Prepare failed for \(provider.name) due to rate limiting or a network error; " + + "skipping cache-clear recovery so cached files stay resumable: \(error)", + source: "ASRService" + ) + throw error + } firstError = error DebugLogger.shared.error("ASRService: First prepare attempt for \(provider.name) failed after \(String(format: "%.2f", Date().timeIntervalSince(start)))s", source: "ASRService") DebugLogger.shared.warning( @@ -3228,6 +3236,32 @@ final class ASRService: ObservableObject { return nsError.domain == NSURLErrorDomain && nsError.code == NSURLErrorCancelled } + /// Rate-limit and transport failures must not trigger the cache-clear recovery: clearing the + /// cache forces a full re-download, which burns more of HuggingFace's fixed per-IP API window + /// and turns a transient 429 into a self-reinforcing failure loop (issue #683). + private nonisolated static func isRateLimitOrNetworkError(_ error: Error) -> Bool { + // Providers may rebox the download error, so walk the underlying-error chain. + var current: Error? = error + var depth = 0 + while let candidate = current, depth < 5 { + if let hfError = candidate as? DownloadUtils.HuggingFaceDownloadError { + switch hfError { + case .rateLimited, .htmlErrorResponse: + return true + case .invalidResponse, .downloadFailed, .modelNotFound: + return false + } + } + let nsError = candidate as NSError + if nsError.domain == NSURLErrorDomain { + return nsError.code != NSURLErrorCancelled + } + current = nsError.userInfo[NSUnderlyingErrorKey] as? Error + depth += 1 + } + return false + } + // MARK: - Model lifecycle helpers (parity with original API) func predownloadSelectedModel() { From 2d4ed6825bea8179055a1a15d46980fd34e86a23 Mon Sep 17 00:00:00 2001 From: Shreeraman A K <16458670+shreeraman96@users.noreply.github.com> Date: Sat, 25 Jul 2026 09:25:23 -0700 Subject: [PATCH 2/3] fix: gate FluidAudio error types behind arch(arm64) for Intel builds The FluidAudio import in ASRService is arm64-only (Intel builds use stubs), so the rate-limit classifier's reference to DownloadUtils.HuggingFaceDownloadError broke the x86_64 archive slice. Compile the HF-specific check out on Intel; the URLError network check still applies there. --- Sources/Fluid/Services/ASRService.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Sources/Fluid/Services/ASRService.swift b/Sources/Fluid/Services/ASRService.swift index 385eaf14..8d1a70e4 100644 --- a/Sources/Fluid/Services/ASRService.swift +++ b/Sources/Fluid/Services/ASRService.swift @@ -3244,6 +3244,7 @@ final class ASRService: ObservableObject { var current: Error? = error var depth = 0 while let candidate = current, depth < 5 { + #if arch(arm64) if let hfError = candidate as? DownloadUtils.HuggingFaceDownloadError { switch hfError { case .rateLimited, .htmlErrorResponse: @@ -3252,6 +3253,7 @@ final class ASRService: ObservableObject { return false } } + #endif let nsError = candidate as NSError if nsError.domain == NSURLErrorDomain { return nsError.code != NSURLErrorCancelled From 1b55a81e5dcd909560ddee9bd0a2eaf0584a65fd Mon Sep 17 00:00:00 2001 From: Shreeraman A K <16458670+shreeraman96@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:47:57 -0700 Subject: [PATCH 3/3] fix: recognize HuggingFaceModelDownloader's HF-domain rate-limit errors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app's own downloader (Nemotron/Cohere engines) throws NSError(domain: "HF", code: ) on HTTP failures, which the rate-limit classifier did not match — so a 429 on those engines still triggered the destructive cache-clear recovery. Treat HF-domain 429/503 as rate limiting too. --- Sources/Fluid/Services/ASRService.swift | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Sources/Fluid/Services/ASRService.swift b/Sources/Fluid/Services/ASRService.swift index 8d1a70e4..628145da 100644 --- a/Sources/Fluid/Services/ASRService.swift +++ b/Sources/Fluid/Services/ASRService.swift @@ -3258,6 +3258,12 @@ final class ASRService: ObservableObject { if nsError.domain == NSURLErrorDomain { return nsError.code != NSURLErrorCancelled } + // The app's own HuggingFaceModelDownloader signals HTTP failures as + // NSError(domain: "HF", code: ) — treat its rate-limit statuses + // the same way so Nemotron/Cohere downloads never wipe cache on a 429. + if nsError.domain == "HF", nsError.code == 429 || nsError.code == 503 { + return true + } current = nsError.userInfo[NSUnderlyingErrorKey] as? Error depth += 1 }