Show raw API error responses on key verification failure

This commit is contained in:
Beingpax 2025-11-20 22:29:21 +05:45
parent cd503ac298
commit b6068bc881
3 changed files with 100 additions and 113 deletions

View File

@ -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()

View File

@ -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)

View File

@ -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