import Foundation import Observation @MainActor @Observable final class SybilSettingsStore { private enum Keys { static let apiBaseURL = "sybil.ios.apiBaseURL" static let adminToken = "sybil.ios.adminToken" static let preferredProvider = "sybil.ios.preferredProvider" static let preferredOpenAIModel = "sybil.ios.preferredOpenAIModel" static let preferredAnthropicModel = "sybil.ios.preferredAnthropicModel" static let preferredXAIModel = "sybil.ios.preferredXAIModel" } private let defaults: UserDefaults var apiBaseURL: String var adminToken: String var preferredProvider: Provider var preferredModelByProvider: [Provider: String] init(defaults: UserDefaults = .standard) { self.defaults = defaults let storedBaseURL = defaults.string(forKey: Keys.apiBaseURL)?.trimmingCharacters(in: .whitespacesAndNewlines) let fallbackBaseURL = "http://127.0.0.1:8787" self.apiBaseURL = storedBaseURL?.isEmpty == false ? storedBaseURL! : fallbackBaseURL self.adminToken = defaults.string(forKey: Keys.adminToken) ?? "" let provider = defaults.string(forKey: Keys.preferredProvider).flatMap(Provider.init(rawValue:)) ?? .openai self.preferredProvider = provider self.preferredModelByProvider = [ .openai: defaults.string(forKey: Keys.preferredOpenAIModel) ?? "gpt-4.1-mini", .anthropic: defaults.string(forKey: Keys.preferredAnthropicModel) ?? "claude-3-5-sonnet-latest", .xai: defaults.string(forKey: Keys.preferredXAIModel) ?? "grok-3-mini" ] } func persist() { defaults.set(apiBaseURL.trimmingCharacters(in: .whitespacesAndNewlines), forKey: Keys.apiBaseURL) let trimmedToken = adminToken.trimmingCharacters(in: .whitespacesAndNewlines) if trimmedToken.isEmpty { defaults.removeObject(forKey: Keys.adminToken) } else { defaults.set(trimmedToken, forKey: Keys.adminToken) } defaults.set(preferredProvider.rawValue, forKey: Keys.preferredProvider) defaults.set(preferredModelByProvider[.openai], forKey: Keys.preferredOpenAIModel) defaults.set(preferredModelByProvider[.anthropic], forKey: Keys.preferredAnthropicModel) defaults.set(preferredModelByProvider[.xai], forKey: Keys.preferredXAIModel) } var trimmedTokenOrNil: String? { let value = adminToken.trimmingCharacters(in: .whitespacesAndNewlines) return value.isEmpty ? nil : value } var normalizedAPIBaseURL: URL? { var raw = apiBaseURL.trimmingCharacters(in: .whitespacesAndNewlines) guard !raw.isEmpty else { return nil } while raw.hasSuffix("/") { raw.removeLast() } guard var components = URLComponents(string: raw) else { return nil } return components.url } }