Files
Termind/TFI/Application/FeatureModels/MobileModelDiscovery.swift
T
Kaede Akino 4ea707083f Support keyless model providers and HTTP endpoints
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.
2026-09-17 08:52:16 +08:00

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
}
}