SPB Git

spb/zyquo-router Public MIT

One local endpoint, every AI provider — a private OpenAI-compatible LLM gateway for your Mac (170 models, 12 providers).

Swift 95.7% Python 2.3% Shell 1.2% Makefile 0.9%
2.8 KB · 73 lines swift
Raw Blame History
1//2//  ModelCatalog.swift3//  Zyquo Router4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//8//  Single source of truth for model data. Built-in entries are generated from9//  docs/PROVIDERS.md (see ModelCatalogData.swift); dynamic /models refreshes and10//  user-defined custom models layer on top. Views and clients never hardcode11//  model IDs.12//1314import Foundation1516@MainActor17final class ModelCatalog: ObservableObject {18    /// Built-in catalog (generated from docs/PROVIDERS.md — keep in sync).19    @Published private(set) var builtIn: [AIModel] = ModelCatalogData.all20    /// User-defined custom models (custom ID + base URL).21    @Published var customModels: [AIModel] = []22    /// Model IDs confirmed live by the last dynamic refresh, per provider.23    @Published private(set) var liveModelIDs: [ProviderID: Set<String>] = [:]24    /// Favorite model IDs, pinned at the top of pickers.25    @Published var favoriteIDs: Set<String> = []2627    var all: [AIModel] { builtIn + customModels }2829    func models(for provider: ProviderID) -> [AIModel] {30        all.filter { $0.provider == provider }31            .sorted { rank($0) < rank($1) }32    }3334    func model(id: String, provider: ProviderID) -> AIModel? {35        all.first { $0.id == id && $0.provider == provider }36    }3738    /// Cheapest non-legacy chat model for a provider (used for key tests and39    /// auto-title generation). Non-reasoning models are preferred — reasoning40    /// models burn their token budget thinking, useless for tiny utility calls.41    func cheapestModel(for provider: ProviderID) -> AIModel? {42        let candidates = models(for: provider).filter { !$0.isLegacy }43        let plain = candidates.filter { !$0.capabilities.reasoning }44        return (plain.isEmpty ? candidates : plain)45            .min { ($0.pricing?.outputPerMTok ?? .infinity) < ($1.pricing?.outputPerMTok ?? .infinity) }46    }4748    /// Default model offered for new conversations.49    var defaultModel: AIModel? {50        all.first { $0.isRecommended } ?? all.first51    }5253    /// Merges a dynamic /models listing: known models are marked live; unknown54    /// IDs are surfaced so the user can add them.55    func applyLiveListing(_ ids: [String], for provider: ProviderID) {56        liveModelIDs[provider] = Set(ids)57    }5859    /// IDs returned by the provider but absent from the built-in catalog.60    func unknownLiveIDs(for provider: ProviderID) -> [String] {61        guard let live = liveModelIDs[provider] else { return [] }62        let known = Set(models(for: provider).map(\.id))63        return live.subtracting(known).sorted()64    }6566    private func rank(_ model: AIModel) -> Int {67        if favoriteIDs.contains(model.id) { return 0 }68        if model.isRecommended { return 1 }69        if model.isLegacy { return 3 }70        return 271    }72}73