121 lines
4.7 KiB
Swift
121 lines
4.7 KiB
Swift
import AgentKit
|
|
import Foundation
|
|
import Observation
|
|
import TermindApplication
|
|
import TermindDomain
|
|
|
|
@MainActor @Observable
|
|
final class MobileProviderEditorModel {
|
|
typealias KeyAction = MobileSecretDraft.Action
|
|
let original: ModelProvider
|
|
let originalEndpoint: ProviderEndpoint
|
|
var draft: ModelProvider
|
|
var endpoint: ProviderEndpoint
|
|
let key = MobileSecretDraft()
|
|
var keyAction: KeyAction {
|
|
get { key.action }
|
|
set { key.action = newValue }
|
|
}
|
|
var secret: String {
|
|
get { key.text }
|
|
set { key.text = newValue }
|
|
}
|
|
private let ai: AIConfigurationModel
|
|
private let wasPersisted: Bool
|
|
|
|
init(ai: AIConfigurationModel, provider: ModelProvider) {
|
|
self.ai = ai
|
|
original = provider
|
|
draft = provider
|
|
wasPersisted = ai.modelProvider(provider.id) != nil
|
|
originalEndpoint = wasPersisted ? ai.endpoint(for: provider.id) : ProviderEndpoint(url: provider.inferenceURL)
|
|
endpoint = originalEndpoint
|
|
key.begin(in: ai, reference: provider.credentialRef)
|
|
if requiresKey { key.action = .replace }
|
|
}
|
|
|
|
var hasExistingKey: Bool { wasPersisted && !original.credentialRef.isEmpty }
|
|
var requiresKey: Bool { !wasPersisted }
|
|
var isDirty: Bool { draft != original || endpoint != originalEndpoint || key.isDirty }
|
|
var canSave: Bool {
|
|
let url = URL(string: endpoint.requestURL)
|
|
return !draft.name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
&& url?.scheme == "https" && url?.host != nil && key.isValid
|
|
}
|
|
|
|
var requestProvider: ModelProvider {
|
|
var provider = draft
|
|
provider.inferenceURL = endpoint.requestURL
|
|
provider.baseURL = endpoint.baseURL
|
|
provider.apiFormat = endpoint.format
|
|
return provider
|
|
}
|
|
|
|
func requestSecret() throws -> String {
|
|
guard !wasPersisted || ai.modelProvider(original.id) != nil else {
|
|
throw MobileEditorError.message(String(localized: "Provider Deleted"))
|
|
}
|
|
return try key.effective(
|
|
in: ai, reference: ai.modelProvider(original.id)?.effectiveCredentialRef ?? draft.effectiveCredentialRef)
|
|
}
|
|
|
|
func add(_ models: [AIModel]) {
|
|
for model in models where !draft.models.contains(where: { $0.id == model.id }) { draft.models.append(model) }
|
|
}
|
|
|
|
func add(id: String) {
|
|
let id = id.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !id.isEmpty else { return }
|
|
add([ModelCapabilityResolver.resolve(model: AIModel(id: id), provider: requestProvider).model])
|
|
}
|
|
|
|
func remove(_ ids: Set<String>) { draft.models.removeAll { ids.contains($0.id) } }
|
|
|
|
func update(_ model: AIModel) {
|
|
guard let index = draft.models.firstIndex(where: { $0.id == model.id }) else { return }
|
|
draft.models[index] = model
|
|
}
|
|
|
|
func save() throws {
|
|
guard canSave else { return }
|
|
let current = ai.modelProvider(original.id)
|
|
guard !wasPersisted || current != nil else {
|
|
throw MobileEditorError.message(String(localized: "Provider Deleted"))
|
|
}
|
|
var value = draft
|
|
var savedEndpoint = endpoint
|
|
if let current {
|
|
var fields = SettingsFields(original, draft, current)
|
|
try fields.merge(\.name)
|
|
try fields.merge(\.isEnabled)
|
|
value = fields.result
|
|
value.models = try SettingsDraftMerge.collection(
|
|
original.models, draft.models, current.models, merge: Self.mergeModel)
|
|
// Discovery time is observational metadata, not an editable conflict.
|
|
if draft.modelsFetchedAt != original.modelsFetchedAt { value.modelsFetchedAt = draft.modelsFetchedAt }
|
|
var connection = SettingsFields(originalEndpoint, endpoint, ai.endpoint(for: original.id))
|
|
try connection.merge(\.url)
|
|
try connection.merge(\.apiType)
|
|
savedEndpoint = connection.result
|
|
}
|
|
value.name = value.name.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
try key.commit(in: ai, reference: value.credentialRef) { reference in
|
|
value.credentialRef = reference
|
|
return ai.saveProvider(value, endpoint: savedEndpoint)
|
|
}
|
|
}
|
|
|
|
static func mergeModel(_ original: AIModel, _ draft: AIModel, _ current: AIModel) throws -> AIModel {
|
|
var fields = SettingsFields(original, draft, current)
|
|
try fields.merge(\.displayName)
|
|
try fields.merge(\.kind)
|
|
try fields.merge(\.input)
|
|
try fields.merge(\.abilities)
|
|
try fields.merge(\.contextLength)
|
|
try fields.merge(\.maxOutputTokens)
|
|
try fields.merge(\.isShownInAssistant)
|
|
try fields.merge(\.reasoningProfile)
|
|
return fields.result
|
|
}
|
|
}
|