spb/zyquo-agent Public MIT
The autonomous agent that actually operates your Mac — plans, runs real commands, verifies its own work.
Swift 94.7%
Shell 4.1%
Python 0.7%
Makefile 0.5%
1//2// ModelCatalog.swift3// Zyquo Agent4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Single source of truth for model data. Built-in entries are the exact9// Zyquo Cloud catalog (ModelCatalogData.swift); dynamic /models refreshes and10// user-defined custom models layer on top. Views and clients never hardcode11// model IDs. Zyquo Agent adds the agent-capable filter and the default agent12// model (docs/PROVIDER-REUSE.md §3).13//1415import Combine16import Foundation1718@MainActor19final class ModelCatalog: ObservableObject {20 /// Built-in catalog (identical to Zyquo Cloud's — keep in sync).21 @Published private(set) var builtIn: [AIModel] = ModelCatalogData.all22 /// User-defined custom models (custom ID + base URL).23 @Published var customModels: [AIModel] = []24 /// Model IDs confirmed live by the last dynamic refresh, per provider.25 @Published private(set) var liveModelIDs: [ProviderID: Set<String>] = [:]26 /// Favorite model IDs, pinned at the top of pickers.27 @Published var favoriteIDs: Set<String> = []2829 var all: [AIModel] { builtIn + customModels }3031 func models(for provider: ProviderID) -> [AIModel] {32 all.filter { $0.provider == provider }33 .sorted { rank($0) < rank($1) }34 }3536 func model(id: String, provider: ProviderID) -> AIModel? {37 all.first { $0.id == id && $0.provider == provider }38 }3940 /// Cheapest non-legacy chat model for a provider (used for key tests and41 /// auto-title generation). Non-reasoning models are preferred — reasoning42 /// models burn their token budget thinking, useless for tiny utility calls.43 func cheapestModel(for provider: ProviderID) -> AIModel? {44 let candidates = models(for: provider).filter { !$0.isLegacy }45 let plain = candidates.filter { !$0.capabilities.reasoning }46 return (plain.isEmpty ? candidates : plain)47 .min { ($0.pricing?.outputPerMTok ?? .infinity) < ($1.pricing?.outputPerMTok ?? .infinity) }48 }4950 /// Default model offered for new conversations.51 var defaultModel: AIModel? {52 all.first { $0.isRecommended } ?? all.first53 }5455 // MARK: - Agent-capable subset (Zyquo Agent addition)5657 /// Models suitable for deep agentic, multi-step tool use — the curated58 /// subset of the shared catalog (docs/PROVIDER-REUSE.md §3).59 var agentCapableModels: [AIModel] {60 all.filter(\.agentCapable)61 }6263 func agentCapableModels(for provider: ProviderID) -> [AIModel] {64 models(for: provider).filter(\.agentCapable)65 }6667 /// Default agent model: Claude Sonnet 5 (falls back to the first68 /// agent-capable model if the catalog ever changes).69 var defaultAgentModel: AIModel? {70 model(id: AgentModelSupport.defaultModelID, provider: AgentModelSupport.defaultModelProvider)71 ?? agentCapableModels.first72 }7374 /// Suggested agent default for one provider (bolded 🤖 entries in the doc).75 func defaultAgentModel(for provider: ProviderID) -> AIModel? {76 if let id = AgentModelSupport.providerDefaults[provider],77 let model = model(id: id, provider: provider), model.agentCapable {78 return model79 }80 return agentCapableModels(for: provider).first81 }8283 /// The recommended top tier, surfaced first in the model chip.84 var recommendedAgentModels: [AIModel] {85 AgentModelSupport.recommendedKeys.compactMap { key in86 let parts = key.split(separator: "|", maxSplits: 1)87 guard parts.count == 2, let provider = ProviderID(rawValue: String(parts[0])) else { return nil }88 return model(id: String(parts[1]), provider: provider)89 }90 }9192 // MARK: - Dynamic listings9394 /// Merges a dynamic /models listing: known models are marked live; unknown95 /// IDs are surfaced so the user can add them.96 func applyLiveListing(_ ids: [String], for provider: ProviderID) {97 liveModelIDs[provider] = Set(ids)98 }99100 /// IDs returned by the provider but absent from the built-in catalog.101 func unknownLiveIDs(for provider: ProviderID) -> [String] {102 guard let live = liveModelIDs[provider] else { return [] }103 let known = Set(models(for: provider).map(\.id))104 return live.subtracting(known).sorted()105 }106107 private func rank(_ model: AIModel) -> Int {108 if favoriteIDs.contains(model.id) { return 0 }109 if model.isRecommended { return 1 }110 if model.isLegacy { return 3 }111 return 2112 }113}114