The base URL only applied on Enter, and applyProviderSelection reset it to the stored value, wiping typed text. Persist edits after a debounce instead, and only load the stored URL when switching provider. Servers like open-webui need a Bearer token, so introduce an optionalAPIKey capability, declare it on the local OpenAI-compatible provider, and show the key field without gating Start on it. Refresh only the model list when the base URL or API key changes, instead of rebuilding the whole provider section: the full rebuild flickered, and hid the API key entry while typing into it. Guard async model loads with a generation counter so stale results cannot overwrite newer ones.
97 lines
2.6 KiB
Swift
97 lines
2.6 KiB
Swift
import Foundation
|
|
|
|
public enum LLMCapability: String, Sendable, Hashable, CaseIterable, Codable {
|
|
case streaming
|
|
case promptCaching
|
|
case thinking
|
|
case toolUse
|
|
case apiKey
|
|
case optionalAPIKey
|
|
case customBaseURL
|
|
}
|
|
|
|
public struct LLMProviderCapabilities: Sendable, Hashable {
|
|
public var supported: Set<LLMCapability>
|
|
public var reasoningEffortOptions: [String]
|
|
public var defaultReasoningEffort: String?
|
|
|
|
public init(
|
|
supported: Set<LLMCapability> = [],
|
|
reasoningEffortOptions: [String] = [],
|
|
defaultReasoningEffort: String? = nil
|
|
) {
|
|
self.supported = supported
|
|
self.reasoningEffortOptions = reasoningEffortOptions
|
|
self.defaultReasoningEffort = defaultReasoningEffort
|
|
}
|
|
|
|
public func supports(_ capability: LLMCapability) -> Bool {
|
|
supported.contains(capability)
|
|
}
|
|
}
|
|
|
|
public struct LLMProviderDescriptor: Sendable, Hashable {
|
|
public var id: String
|
|
public var displayName: String
|
|
public var capabilities: LLMProviderCapabilities
|
|
public var defaultModelID: String?
|
|
public var summarizationModelID: String?
|
|
public var defaultBaseURL: URL
|
|
|
|
public init(
|
|
id: String,
|
|
displayName: String,
|
|
capabilities: LLMProviderCapabilities,
|
|
defaultModelID: String?,
|
|
summarizationModelID: String? = nil,
|
|
defaultBaseURL: URL
|
|
) {
|
|
self.id = id
|
|
self.displayName = displayName
|
|
self.capabilities = capabilities
|
|
self.defaultModelID = defaultModelID
|
|
self.summarizationModelID = summarizationModelID
|
|
self.defaultBaseURL = defaultBaseURL
|
|
}
|
|
}
|
|
|
|
public protocol LLMProvider: Sendable {
|
|
var descriptor: LLMProviderDescriptor { get }
|
|
|
|
func streamTurn(
|
|
_ request: LLMTurnRequest,
|
|
apiKey: String?,
|
|
baseURL: URL?
|
|
) -> AsyncThrowingStream<LLMTurnEvent, Error>
|
|
|
|
func suggestedModels(apiKey: String?, baseURL: URL?) async throws -> [LLMModelInfo]
|
|
}
|
|
|
|
@MainActor
|
|
public final class LLMProviderRegistry {
|
|
private var providersByID: [String: any LLMProvider] = [:]
|
|
private var orderedIDs: [String] = []
|
|
|
|
public init() {}
|
|
|
|
public func register(_ provider: any LLMProvider) {
|
|
let id = provider.descriptor.id
|
|
if providersByID[id] == nil {
|
|
orderedIDs.append(id)
|
|
}
|
|
providersByID[id] = provider
|
|
}
|
|
|
|
public func provider(id: String) -> (any LLMProvider)? {
|
|
providersByID[id]
|
|
}
|
|
|
|
public func providers() -> [any LLMProvider] {
|
|
orderedIDs.compactMap { providersByID[$0] }
|
|
}
|
|
|
|
public func descriptors() -> [LLMProviderDescriptor] {
|
|
providers().map(\.descriptor)
|
|
}
|
|
}
|