SPB Git

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%
4.4 KB · 114 lines swift
Raw Blame History
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