Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/screenshots/oauth-login-providers.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 3 additions & 1 deletion Sources/Fluid/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1861,7 +1861,7 @@ struct ContentView: View {
let isLocal = self.isLocalEndpoint(derivedBaseURL)
let apiKey = route.apiKey

if !isLocal {
if !isLocal, !OfficialProviderAuth.isOfficialProvider(currentSelectedProviderID) {
guard !apiKey.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines).isEmpty else {
throw AIProcessingError.missingAPIKey(provider: derivedCurrentProvider)
}
Expand Down Expand Up @@ -1939,6 +1939,7 @@ struct ContentView: View {

// Build LLMClient configuration
var config = LLMClient.Config(
providerID: currentSelectedProviderID,
messages: messages,
model: derivedSelectedModel,
baseURL: derivedBaseURL,
Expand Down Expand Up @@ -1966,6 +1967,7 @@ struct ContentView: View {
source: "ContentView"
)
let fallbackConfig = LLMClient.Config(
providerID: currentSelectedProviderID,
messages: messages,
model: derivedSelectedModel,
baseURL: derivedBaseURL,
Comment thread
Elemperor1 marked this conversation as resolved.
Expand Down
59 changes: 58 additions & 1 deletion Sources/Fluid/Persistence/ChatHistoryStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ struct ChatMessage: Codable, Identifiable, Equatable {
let role: Role
let content: String
let toolCall: ToolCall?
let responsesContinuationItems: [LLMClient.ResponsesContinuationItem]
let responsesContinuationScope: String?
let stepType: StepType
let timestamp: Date

Expand All @@ -39,16 +41,71 @@ struct ChatMessage: Codable, Identifiable, Equatable {
let command: String
let workingDirectory: String?
let purpose: String?
let thoughtSignature: String?

init(
id: String,
command: String,
workingDirectory: String?,
purpose: String?,
thoughtSignature: String? = nil
) {
self.id = id
self.command = command
self.workingDirectory = workingDirectory
self.purpose = purpose
self.thoughtSignature = thoughtSignature
}
}

init(id: UUID = UUID(), role: Role, content: String, toolCall: ToolCall? = nil, stepType: StepType = .normal, timestamp: Date = Date()) {
init(
id: UUID = UUID(),
role: Role,
content: String,
toolCall: ToolCall? = nil,
responsesContinuationItems: [LLMClient.ResponsesContinuationItem] = [],
responsesContinuationScope: String? = nil,
stepType: StepType = .normal,
timestamp: Date = Date()
) {
self.id = id
self.role = role
self.content = content
self.toolCall = toolCall
self.responsesContinuationItems = responsesContinuationItems
self.responsesContinuationScope = responsesContinuationScope
self.stepType = stepType
self.timestamp = timestamp
}

private enum CodingKeys: String, CodingKey {
case id
case role
case content
case toolCall
case responsesContinuationItems
case responsesContinuationScope
case stepType
case timestamp
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(UUID.self, forKey: .id)
self.role = try container.decode(Role.self, forKey: .role)
self.content = try container.decode(String.self, forKey: .content)
self.toolCall = try container.decodeIfPresent(ToolCall.self, forKey: .toolCall)
self.responsesContinuationItems = try container.decodeIfPresent(
[LLMClient.ResponsesContinuationItem].self,
forKey: .responsesContinuationItems
) ?? []
self.responsesContinuationScope = try container.decodeIfPresent(
String.self,
forKey: .responsesContinuationScope
)
self.stepType = try container.decode(StepType.self, forKey: .stepType)
self.timestamp = try container.decode(Date.self, forKey: .timestamp)
}
}

// MARK: - Chat Session Model
Expand Down
16 changes: 7 additions & 9 deletions Sources/Fluid/Persistence/SettingsStore+CommandMode.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Combine
import CryptoKit
import Foundation

extension SettingsStore {
Expand Down Expand Up @@ -101,7 +100,7 @@ extension SettingsStore {

let baseURL = self.commandModeProviderBaseURL(for: providerID)
let apiKey = self.getAPIKey(for: providerID) ?? ""
return self.commandModeProviderFingerprint(baseURL: baseURL, apiKey: apiKey) == stored
return self.commandModeProviderFingerprint(providerID: providerID, baseURL: baseURL, apiKey: apiKey) == stored
}

private func commandModeProviderBaseURL(for providerID: String) -> String {
Expand All @@ -114,13 +113,12 @@ extension SettingsStore {
return ""
}

private func commandModeProviderFingerprint(baseURL: String, apiKey: String) -> String? {
let trimmedBase = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedBase.isEmpty else { return nil }
let input = "\(trimmedBase)|\(trimmedKey)"
let digest = SHA256.hash(data: Data(input.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
private func commandModeProviderFingerprint(providerID: String, baseURL: String, apiKey: String) -> String? {
OfficialProviderAuth.configurationFingerprint(
providerID: providerID,
baseURL: baseURL,
apiKey: apiKey
)
}

private func isPrivateAIProviderID(_ providerID: String) -> Bool {
Expand Down
24 changes: 12 additions & 12 deletions Sources/Fluid/Persistence/SettingsStore.swift
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import AppKit
import ApplicationServices
import Combine
import CryptoKit
import Foundation
import ServiceManagement
import SwiftUI
Expand Down Expand Up @@ -1668,7 +1667,7 @@ final class SettingsStore: ObservableObject {
let hasDefaultModel = !ModelRepository.shared.defaultModels(for: providerID).isEmpty
let hasModel = hasSelectedModel || hasDefaultModel

return (isLocal || hasApiKey) && hasModel
return (isLocal || hasApiKey || OfficialProviderAuth.isOfficialProvider(providerID)) && hasModel
}

/// The base URL for the currently selected AI provider
Expand Down Expand Up @@ -3587,9 +3586,12 @@ final class SettingsStore: ObservableObject {

let baseURL = self.providerBaseURLForVerification(for: trimmed)
let apiKey = (self.getAPIKey(for: trimmed) ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
guard ModelRepository.shared.isLocalEndpoint(baseURL) || !apiKey.isEmpty else { return false }
guard ModelRepository.shared.isLocalEndpoint(baseURL) ||
OfficialProviderAuth.isOfficialProvider(trimmed) ||
!apiKey.isEmpty
else { return false }

return self.providerFingerprint(baseURL: baseURL, apiKey: apiKey) == stored
return self.providerFingerprint(providerID: trimmed, baseURL: baseURL, apiKey: apiKey) == stored
}

private func providerBaseURLForVerification(for providerID: String) -> String {
Expand All @@ -3604,14 +3606,12 @@ final class SettingsStore: ObservableObject {
return ""
}

private func providerFingerprint(baseURL: String, apiKey: String) -> String? {
let trimmedBaseURL = baseURL.trimmingCharacters(in: .whitespacesAndNewlines)
let trimmedAPIKey = apiKey.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmedBaseURL.isEmpty else { return nil }

let input = "\(trimmedBaseURL)|\(trimmedAPIKey)"
let digest = SHA256.hash(data: Data(input.utf8))
return digest.map { String(format: "%02x", $0) }.joined()
private func providerFingerprint(providerID: String, baseURL: String, apiKey: String) -> String? {
OfficialProviderAuth.configurationFingerprint(
providerID: providerID,
baseURL: baseURL,
apiKey: apiKey
)
}

private func syncLinkedProviderSelections(to providerID: String) {
Expand Down
Loading
Loading