diff --git a/oAI.xcodeproj/project.pbxproj b/oAI.xcodeproj/project.pbxproj index b64b9e8..177d8e0 100644 --- a/oAI.xcodeproj/project.pbxproj +++ b/oAI.xcodeproj/project.pbxproj @@ -279,7 +279,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 2.3.4; + MARKETING_VERSION = 2.3.5; PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; @@ -323,7 +323,7 @@ LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; MACOSX_DEPLOYMENT_TARGET = 26.2; - MARKETING_VERSION = 2.3.4; + MARKETING_VERSION = 2.3.5; PRODUCT_BUNDLE_IDENTIFIER = com.oai.oAI; PRODUCT_NAME = "$(TARGET_NAME)"; REGISTER_APP_GROUPS = YES; diff --git a/oAI/Services/EmbeddingService.swift b/oAI/Services/EmbeddingService.swift index 5bd4132..391f1d5 100644 --- a/oAI/Services/EmbeddingService.swift +++ b/oAI/Services/EmbeddingService.swift @@ -75,6 +75,16 @@ final class EmbeddingService { private let settings = SettingsService.shared + /// Dedicated session for embedding requests — keeps embedding traffic isolated + /// from the chat API sessions and self-limits concurrent connections. + private let session: URLSession = { + let config = URLSessionConfiguration.default + config.httpMaximumConnectionsPerHost = 2 // max 2 concurrent embedding requests + config.timeoutIntervalForRequest = 60 + config.timeoutIntervalForResource = 120 + return URLSession(configuration: config) + }() + private init() {} // MARK: - Provider Detection @@ -164,7 +174,7 @@ final class EmbeddingService { ] request.httpBody = try JSONSerialization.data(withJSONObject: body) - let (data, response) = try await URLSession.shared.data(for: request) + let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw EmbeddingError.invalidResponse @@ -205,7 +215,7 @@ final class EmbeddingService { ] request.httpBody = try JSONSerialization.data(withJSONObject: body) - let (data, response) = try await URLSession.shared.data(for: request) + let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw EmbeddingError.invalidResponse @@ -247,7 +257,7 @@ final class EmbeddingService { ] request.httpBody = try JSONSerialization.data(withJSONObject: body) - let (data, response) = try await URLSession.shared.data(for: request) + let (data, response) = try await session.data(for: request) guard let httpResponse = response as? HTTPURLResponse else { throw EmbeddingError.invalidResponse diff --git a/oAI/ViewModels/ChatViewModel.swift b/oAI/ViewModels/ChatViewModel.swift index 398a25f..f14e4cd 100644 --- a/oAI/ViewModels/ChatViewModel.swift +++ b/oAI/ViewModels/ChatViewModel.swift @@ -1793,16 +1793,15 @@ Don't narrate future actions ("Let me...") - just use the tools. await checkAndSummarizeOldMessages(conversationId: conversation.id) } - // Generate embeddings for messages that don't have them yet + // Generate embeddings for messages that don't have them yet. + // Run sequentially at background priority so this never blocks the chat. if settings.embeddingsEnabled { - Task { + Task(priority: .background) { + guard let provider = EmbeddingService.shared.getSelectedProvider() else { return } for message in chatMessages { - // Skip if already embedded - if let _ = try? EmbeddingService.shared.getMessageEmbedding(messageId: message.id) { - continue - } - // Generate embedding (this will now succeed since message is in DB) - generateEmbeddingForMessage(message) + await embedMessage(message, provider: provider) + // Yield briefly between requests to avoid bursting the API + try? await Task.sleep(for: .milliseconds(150)) } } } @@ -1927,38 +1926,44 @@ Don't narrate future actions ("Let me...") - just use the tools. // MARK: - Embedding Generation - /// Generate embedding for a message in the background + /// Generate embedding for a single message (awaitable, no Task spawned). + /// Call this directly when you want to sequence or rate-limit requests. + private func embedMessage(_ message: Message, provider: EmbeddingProvider) async { + guard message.content.count > 20 else { return } + // Skip if already embedded + if let _ = try? EmbeddingService.shared.getMessageEmbedding(messageId: message.id) { return } + do { + let embedding = try await EmbeddingService.shared.generateEmbedding( + text: message.content, + provider: provider + ) + try EmbeddingService.shared.saveMessageEmbedding( + messageId: message.id, + embedding: embedding, + model: provider.defaultModel + ) + Log.api.info("Generated embedding for message \(message.id) using \(provider.displayName)") + } catch { + let errorString = String(describing: error) + if errorString.contains("FOREIGN KEY constraint failed") { + Log.api.debug("Message \(message.id) not in database yet - will embed later during save or batch operation") + } else { + Log.api.error("Failed to generate embedding for message \(message.id): \(error)") + } + } + } + + /// Fire-and-forget wrapper — runs at background priority so it never blocks the chat. func generateEmbeddingForMessage(_ message: Message) { guard settings.embeddingsEnabled else { return } - guard message.content.count > 20 else { return } // Skip very short messages + guard message.content.count > 20 else { return } - Task { - do { - // Use user's selected provider, or fall back to best available - guard let provider = EmbeddingService.shared.getSelectedProvider() else { - Log.api.warning("No embedding providers available - skipping embedding generation") - return - } - - let embedding = try await EmbeddingService.shared.generateEmbedding( - text: message.content, - provider: provider - ) - try EmbeddingService.shared.saveMessageEmbedding( - messageId: message.id, - embedding: embedding, - model: provider.defaultModel - ) - Log.api.info("Generated embedding for message \(message.id) using \(provider.displayName)") - } catch { - // Check if it's a foreign key constraint error (message not saved to DB yet) - let errorString = String(describing: error) - if errorString.contains("FOREIGN KEY constraint failed") { - Log.api.debug("Message \(message.id) not in database yet - will embed later during save or batch operation") - } else { - Log.api.error("Failed to generate embedding for message \(message.id): \(error)") - } + Task(priority: .background) { + guard let provider = EmbeddingService.shared.getSelectedProvider() else { + Log.api.warning("No embedding providers available - skipping embedding generation") + return } + await embedMessage(message, provider: provider) } }