Allow optional API keys in desktop and mobile settings and share credential resolution across model requests. Missing stored credentials remain errors. Enable user-configured HTTP endpoints and pin signed AgentKit 0.13.1, which omits empty authentication headers for probes and inference. Validation: AgentKit's 216 tests and iOS's 461 tests passed, along with Swift formatting, localization checks, native screenshots, and real HTTP smoke tests on both apps. macOS passed 1419 of 1422 tests initially; existing Metrics timing, waveform PNG comparison, and MCP upload runner failures were observed. MCP and Metrics passed a focused rerun, while Metrics remains intermittent and waveform comparison still fails. iOS simulator LAN-IP access timed out; desktop LAN-IP and both platforms' localhost, loopback-IP, and local-domain requests passed.
64 lines
2.1 KiB
Swift
64 lines
2.1 KiB
Swift
import AgentKit
|
|
import Foundation
|
|
import Observation
|
|
import TermindApplication
|
|
|
|
@MainActor
|
|
@Observable
|
|
final class MobileModelDiscovery {
|
|
private(set) var candidates: [AIModel] = []
|
|
private(set) var running = false
|
|
var error: String?
|
|
private var task: Task<Void, Never>?
|
|
private var generation = UUID()
|
|
typealias Fetch = @Sendable (ModelProvider, String) async throws -> [AIModel]
|
|
private let fetch: Fetch
|
|
|
|
init(fetch: @escaping Fetch = { try await ModelCatalogClient().models(for: $0, secret: $1) }) {
|
|
self.fetch = fetch
|
|
}
|
|
|
|
func start(ai: AIConfigurationModel, provider: ModelProvider) {
|
|
cancel()
|
|
do {
|
|
guard let secret = ai.modelSecret(for: provider) else {
|
|
throw MobileEditorError.message(
|
|
ai.secretStorageError ?? String(localized: "The provider's credential is unavailable."))
|
|
}
|
|
start(provider: provider, secret: secret, isCurrent: { ai.modelProvider(provider.id) == provider })
|
|
} catch { self.error = error.localizedDescription }
|
|
}
|
|
|
|
func start(provider: ModelProvider, secret: String, isCurrent: @escaping @MainActor () -> Bool = { true }) {
|
|
cancel()
|
|
let token = generation
|
|
running = true
|
|
error = nil
|
|
task = Task { [weak self, fetch] in
|
|
defer { if self?.generation == token { self?.running = false } }
|
|
do {
|
|
let values = try await fetch(provider, secret)
|
|
guard let self, generation == token, !Task.isCancelled,
|
|
isCurrent()
|
|
else { return }
|
|
var seen = Set<String>()
|
|
candidates = values.filter { seen.insert($0.id).inserted }
|
|
running = false
|
|
} catch {
|
|
guard let self, generation == token, !Task.isCancelled, isCurrent() else { return }
|
|
self.error = error.localizedDescription
|
|
running = false
|
|
}
|
|
}
|
|
}
|
|
|
|
func cancel() {
|
|
generation = UUID()
|
|
task?.cancel()
|
|
task = nil
|
|
candidates = []
|
|
error = nil
|
|
running = false
|
|
}
|
|
}
|