Show raw API error responses on key verification failure
This commit is contained in:
parent
cd503ac298
commit
b6068bc881
@ -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()
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user