From b6068bc88194f99c260060ec49afc8091a0763ae Mon Sep 17 00:00:00 2001 From: Beingpax Date: Thu, 20 Nov 2025 22:29:21 +0545 Subject: [PATCH] Show raw API error responses on key verification failure --- .../Services/AIEnhancement/AIService.swift | 121 +++++++++--------- .../AI Models/APIKeyManagementView.swift | 74 ++++------- .../AI Models/CloudModelCardRowView.swift | 18 ++- 3 files changed, 100 insertions(+), 113 deletions(-) diff --git a/VoiceInk/Services/AIEnhancement/AIService.swift b/VoiceInk/Services/AIEnhancement/AIService.swift index d4a1a87..41dca21 100644 --- a/VoiceInk/Services/AIEnhancement/AIService.swift +++ b/VoiceInk/Services/AIEnhancement/AIService.swift @@ -1,5 +1,4 @@ import Foundation -import os enum AIProvider: String, CaseIterable { case cerebras = "Cerebras" @@ -149,8 +148,6 @@ enum AIProvider: String, CaseIterable { } class AIService: ObservableObject { - private let logger = Logger(subsystem: "com.prakashjoshipax.voiceink", category: "AIService") - @Published var apiKey: String = "" @Published var isAPIKeyValid: Bool = false @Published var customBaseURL: String = UserDefaults.standard.string(forKey: "customProviderBaseURL") ?? "" { @@ -278,13 +275,13 @@ class AIService: ObservableObject { NotificationCenter.default.post(name: .AppSettingsDidChange, object: nil) } - func saveAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + func saveAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { guard selectedProvider.requiresAPIKey else { - completion(true) + completion(true, nil) return } - verifyAPIKey(key) { [weak self] isValid in + verifyAPIKey(key) { [weak self] isValid, errorMessage in guard let self = self else { return } DispatchQueue.main.async { if isValid { @@ -295,14 +292,14 @@ class AIService: ObservableObject { } else { self.isAPIKeyValid = false } - completion(isValid) + completion(isValid, errorMessage) } } } - func verifyAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + func verifyAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { guard selectedProvider.requiresAPIKey else { - completion(true) + completion(true, nil) return } @@ -322,7 +319,7 @@ class AIService: ObservableObject { } } - private func verifyOpenAICompatibleAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + private func verifyOpenAICompatibleAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { let url = URL(string: selectedProvider.baseURL)! var request = URLRequest(url: url) request.httpMethod = "POST" @@ -338,12 +335,9 @@ class AIService: ObservableObject { request.httpBody = try? JSONSerialization.data(withJSONObject: testBody) - logger.notice("🔑 Verifying API key for \(self.selectedProvider.rawValue, privacy: .public) provider at \(url.absoluteString, privacy: .public)") - URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - self.logger.notice("🔑 API key verification failed for \(self.selectedProvider.rawValue, privacy: .public): \(error.localizedDescription, privacy: .public)") - completion(false) + completion(false, error.localizedDescription) return } @@ -351,23 +345,21 @@ class AIService: ObservableObject { let isValid = httpResponse.statusCode == 200 if !isValid { - // Log the exact API error response - if let data = data, let exactAPIError = String(data: data, encoding: .utf8) { - self.logger.notice("🔑 API key verification failed for \(self.selectedProvider.rawValue, privacy: .public) - Status: \(httpResponse.statusCode) - \(exactAPIError, privacy: .public)") + if let data = data, let responseString = String(data: data, encoding: .utf8) { + completion(false, responseString) } else { - self.logger.notice("🔑 API key verification failed for \(self.selectedProvider.rawValue, privacy: .public) - Status: \(httpResponse.statusCode)") + completion(false, nil) } + } else { + completion(true, nil) } - - completion(isValid) } else { - self.logger.notice("🔑 API key verification failed for \(self.selectedProvider.rawValue, privacy: .public): Invalid response") - completion(false) + completion(false, nil) } }.resume() } - private func verifyAnthropicAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + private func verifyAnthropicAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { let url = URL(string: selectedProvider.baseURL)! var request = URLRequest(url: url) request.httpMethod = "POST" @@ -388,19 +380,27 @@ class AIService: ObservableObject { URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - completion(false) + completion(false, error.localizedDescription) return } if let httpResponse = response as? HTTPURLResponse { - completion(httpResponse.statusCode == 200) + if httpResponse.statusCode == 200 { + completion(true, nil) + } else { + if let data = data, let responseString = String(data: data, encoding: .utf8) { + completion(false, responseString) + } else { + completion(false, nil) + } + } } else { - completion(false) + completion(false, nil) } }.resume() } - private func verifyElevenLabsAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + private func verifyElevenLabsAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { let url = URL(string: "https://api.elevenlabs.io/v1/user")! var request = URLRequest(url: url) @@ -412,14 +412,17 @@ class AIService: ObservableObject { let isValid = (response as? HTTPURLResponse)?.statusCode == 200 if let data = data, let body = String(data: data, encoding: .utf8) { - self.logger.info("ElevenLabs verification response: \(body)") + if !isValid { + completion(false, body) + return + } } - completion(isValid) + completion(isValid, nil) }.resume() } - private func verifyMistralAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + private func verifyMistralAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { let url = URL(string: "https://api.mistral.ai/v1/models")! var request = URLRequest(url: url) request.httpMethod = "GET" @@ -427,30 +430,27 @@ class AIService: ObservableObject { URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - self.logger.error("Mistral API key verification failed: \(error.localizedDescription)") - completion(false) + completion(false, error.localizedDescription) return } if let httpResponse = response as? HTTPURLResponse { if httpResponse.statusCode == 200 { - completion(true) + completion(true, nil) } else { if let data = data, let body = String(data: data, encoding: .utf8) { - self.logger.error("Mistral API key verification failed with status code \(httpResponse.statusCode): \(body)") + completion(false, body) } else { - self.logger.error("Mistral API key verification failed with status code \(httpResponse.statusCode) and no response body.") + completion(false, nil) } - completion(false) } } else { - self.logger.error("Mistral API key verification failed: Invalid response from server.") - completion(false) + completion(false, nil) } }.resume() } - private func verifyDeepgramAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + private func verifyDeepgramAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { let url = URL(string: "https://api.deepgram.com/v1/auth/token")! var request = URLRequest(url: url) request.httpMethod = "GET" @@ -458,22 +458,29 @@ class AIService: ObservableObject { URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - self.logger.error("Deepgram API key verification failed: \(error.localizedDescription)") - completion(false) + completion(false, error.localizedDescription) return } if let httpResponse = response as? HTTPURLResponse { - completion(httpResponse.statusCode == 200) + if httpResponse.statusCode == 200 { + completion(true, nil) + } else { + if let data = data, let responseString = String(data: data, encoding: .utf8) { + completion(false, responseString) + } else { + completion(false, nil) + } + } } else { - completion(false) + completion(false, nil) } }.resume() } - private func verifySonioxAPIKey(_ key: String, completion: @escaping (Bool) -> Void) { + private func verifySonioxAPIKey(_ key: String, completion: @escaping (Bool, String?) -> Void) { guard let url = URL(string: "https://api.soniox.com/v1/files") else { - completion(false) + completion(false, nil) return } var request = URLRequest(url: url) @@ -481,17 +488,24 @@ class AIService: ObservableObject { request.addValue("Bearer \(key)", forHTTPHeaderField: "Authorization") request.addValue("application/json", forHTTPHeaderField: "Accept") - URLSession.shared.dataTask(with: request) { _, response, error in + URLSession.shared.dataTask(with: request) { data, response, error in if let error = error { - self.logger.error("Soniox API key verification failed: \(error.localizedDescription)") - completion(false) + completion(false, error.localizedDescription) return } if let httpResponse = response as? HTTPURLResponse { - completion(httpResponse.statusCode == 200) + if httpResponse.statusCode == 200 { + completion(true, nil) + } else { + if let data = data, let responseString = String(data: data, encoding: .utf8) { + completion(false, responseString) + } else { + completion(false, nil) + } + } } else { - completion(false) + completion(false, nil) } }.resume() } @@ -521,13 +535,10 @@ class AIService: ObservableObject { } func enhanceWithOllama(text: String, systemPrompt: String) async throws -> String { - logger.notice("🔄 Sending transcription to Ollama for enhancement (model: \(self.ollamaService.selectedModel))") do { let result = try await ollamaService.enhance(text, withSystemPrompt: systemPrompt) - logger.notice("✅ Ollama enhancement completed successfully (\(result.count) characters)") return result } catch { - logger.notice("❌ Ollama enhancement failed: \(error.localizedDescription)") throw error } } @@ -552,7 +563,6 @@ class AIService: ObservableObject { let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { - logger.error("Failed to fetch OpenRouter models: Invalid HTTP response") await MainActor.run { self.openRouterModels = [] self.saveOpenRouterModels() @@ -563,7 +573,6 @@ class AIService: ObservableObject { guard let jsonResponse = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let dataArray = jsonResponse["data"] as? [[String: Any]] else { - logger.error("Failed to parse OpenRouter models JSON") await MainActor.run { self.openRouterModels = [] self.saveOpenRouterModels() @@ -581,10 +590,8 @@ class AIService: ObservableObject { } self.objectWillChange.send() } - logger.info("Successfully fetched \(models.count) OpenRouter models.") } catch { - logger.error("Error fetching OpenRouter models: \(error.localizedDescription)") await MainActor.run { self.openRouterModels = [] self.saveOpenRouterModels() diff --git a/VoiceInk/Views/AI Models/APIKeyManagementView.swift b/VoiceInk/Views/AI Models/APIKeyManagementView.swift index 45df228..d502b9a 100644 --- a/VoiceInk/Views/AI Models/APIKeyManagementView.swift +++ b/VoiceInk/Views/AI Models/APIKeyManagementView.swift @@ -196,29 +196,6 @@ struct APIKeyManagementView: View { .buttonStyle(.bordered) .controlSize(.small) } - - // Help text for troubleshooting - if ollamaModels.isEmpty { - VStack(alignment: .leading, spacing: 8) { - Text("Troubleshooting") - .font(.subheadline) - .bold() - - VStack(alignment: .leading, spacing: 4) { - bulletPoint("Ensure Ollama is installed and running") - bulletPoint("Check if the server URL is correct") - bulletPoint("Verify you have at least one model pulled") - } - - Button("Learn More") { - NSWorkspace.shared.open(URL(string: "https://ollama.ai/download")!) - } - .font(.caption) - } - .padding(12) - .background(Color.secondary.opacity(0.05)) - .cornerRadius(8) - } } .padding() .background(Color.secondary.opacity(0.03)) @@ -293,28 +270,28 @@ struct APIKeyManagementView: View { .font(.system(.body, design: .monospaced)) HStack { - Button(action: { - isVerifying = true - aiService.saveAPIKey(apiKey) { success in - isVerifying = false - if !success { - alertMessage = "Invalid API key. Please check and try again." - showAlert = true - } - apiKey = "" - } - }) { - HStack { - if isVerifying { - ProgressView() - .scaleEffect(0.5) - .frame(width: 16, height: 16) - } else { - Image(systemName: "checkmark.circle.fill") - } - Text("Verify and Save") + Button(action: { + isVerifying = true + aiService.saveAPIKey(apiKey) { success, errorMessage in + isVerifying = false + if !success { + alertMessage = errorMessage ?? "Verification failed" + showAlert = true } + apiKey = "" } + }) { + HStack { + if isVerifying { + ProgressView() + .scaleEffect(0.5) + .frame(width: 16, height: 16) + } else { + Image(systemName: "checkmark.circle.fill") + } + Text("Verify and Save") + } + } .disabled(aiService.customBaseURL.isEmpty || aiService.customModel.isEmpty || apiKey.isEmpty) Spacer() @@ -362,10 +339,10 @@ struct APIKeyManagementView: View { HStack { Button(action: { isVerifying = true - aiService.saveAPIKey(apiKey) { success in + aiService.saveAPIKey(apiKey) { success, errorMessage in isVerifying = false if !success { - alertMessage = "Invalid API key. Please check and try again." + alertMessage = errorMessage ?? "Verification failed" showAlert = true } apiKey = "" @@ -467,13 +444,6 @@ struct APIKeyManagementView: View { } } - private func bulletPoint(_ text: String) -> some View { - HStack(alignment: .top, spacing: 4) { - Text("•") - Text(text) - } - } - private func formatSize(_ bytes: Int64) -> String { let gigabytes = Double(bytes) / 1_000_000_000 return String(format: "%.1f GB", gigabytes) diff --git a/VoiceInk/Views/AI Models/CloudModelCardRowView.swift b/VoiceInk/Views/AI Models/CloudModelCardRowView.swift index d00cc1a..4db2b19 100644 --- a/VoiceInk/Views/AI Models/CloudModelCardRowView.swift +++ b/VoiceInk/Views/AI Models/CloudModelCardRowView.swift @@ -14,6 +14,7 @@ struct CloudModelCardView: View { @State private var isVerifying = false @State private var verificationStatus: VerificationStatus = .none @State private var isConfiguredState: Bool = false + @State private var verificationError: String? = nil enum VerificationStatus { case none, verifying, success, failure @@ -248,9 +249,15 @@ struct CloudModelCardView: View { } if verificationStatus == .failure { - Text("Invalid API key. Please check your key and try again.") - .font(.caption) - .foregroundColor(Color(.systemRed)) + if let error = verificationError { + Text(error) + .font(.caption) + .foregroundColor(Color(.systemRed)) + } else { + Text("Verification failed") + .font(.caption) + .foregroundColor(Color(.systemRed)) + } } else if verificationStatus == .success { Text("API key verified successfully!") .font(.caption) @@ -293,11 +300,12 @@ struct CloudModelCardView: View { return } - aiService.saveAPIKey(apiKey) { isValid in + aiService.saveAPIKey(apiKey) { isValid, errorMessage in DispatchQueue.main.async { self.isVerifying = false if isValid { self.verificationStatus = .success + self.verificationError = nil // Save the API key UserDefaults.standard.set(self.apiKey, forKey: "\(self.providerKey)APIKey") self.isConfiguredState = true @@ -308,6 +316,7 @@ struct CloudModelCardView: View { } } else { self.verificationStatus = .failure + self.verificationError = errorMessage } // Restore original provider @@ -320,6 +329,7 @@ struct CloudModelCardView: View { UserDefaults.standard.removeObject(forKey: "\(providerKey)APIKey") apiKey = "" verificationStatus = .none + verificationError = nil isConfiguredState = false // If this model is currently the default, clear it