SPB Git

spb/zyquo-local Public MIT

Native macOS AI chat that runs LLMs 100% locally on Apple Silicon with MLX — no cloud, no API keys.

Swift 97.2% Shell 1.8% Makefile 1%
5.2 KB · 132 lines swift
Raw Blame History
1//2//  HubPoCRunner.swift3//  Zyquo Local4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import Foundation1011/// Phase 3 gate: `ZyquoLocal --hub-poc` proves search → download with live12/// progress → pause mid-flight → resume → validate → delete, end to end,13/// against the live Hugging Face Hub, in an isolated temporary Models root.14enum HubPoCRunner {15    private static let testRepo = "mlx-community/Qwen3-0.6B-4bit"1617    @MainActor18    static func run() async {19        let hub = HubService(token: nil)2021        // 1 — live search22        log("1) Live Hub search: “qwen” in mlx-community, by downloads…")23        do {24            let results = try await hub.search(query: "qwen", scope: .mlxCommunity, limit: 5)25            for r in results {26                log("   • \(r.id)\(r.downloads ?? 0)  arch=\(r.architecture ?? "?")\(r.isSupportedArchitecture ? "" : "  ⚠️ unsupported")")27            }28            guard !results.isEmpty else { return fail("search returned nothing") }29        } catch {30            return fail("search: \(error.localizedDescription)")31        }3233        // Isolated store for the test34        let root = FileManager.default.temporaryDirectory35            .appendingPathComponent("ZyquoLocalHubPoC-\(UUID().uuidString)")36        let store = ModelStore(root: root)37        let manager = DownloadManager(hub: hub, store: store)38        defer { try? FileManager.default.removeItem(at: root) }3940        // 2 — file listing + size41        log("2) File listing for \(testRepo)…")42        do {43            let (files, total) = try await hub.requiredFiles(of: testRepo)44            log("   \(files.count) required files, total \(gb(total))")45        } catch {46            return fail("file listing: \(error.localizedDescription)")47        }4849        // 3 — download with live progress, pause at ≥25 %50        log("3) Downloading with live progress; pausing mid-flight…")51        await manager.download(repoID: testRepo)52        var paused = false53        while !paused {54            try? await Task.sleep(for: .milliseconds(200))55            guard let t = manager.task(for: testRepo) else { return fail("task vanished") }56            if t.state == .failed { return fail("download failed: \(t.errorDescription ?? "?")") }57            if t.fractionCompleted >= 0.25 {58                manager.pause(repoID: testRepo)59                paused = true60                let speed = manager.speeds[testRepo].map { "\(gb(Int64($0)))/s" } ?? "n/a"61                log("   paused at \(pct(t.fractionCompleted)) (speed was \(speed))")62            }63        }64        try? await Task.sleep(for: .milliseconds(400))6566        // 4 — verify partials survive, then resume67        guard let pausedTask = manager.task(for: testRepo), pausedTask.state == .paused else {68            return fail("expected paused state")69        }70        let partialBytes = pausedTask.receivedBytes71        guard partialBytes > 0 else { return fail("no partial bytes on disk") }72        log("4) Resuming from \(gb(partialBytes)) (HTTP Range)…")73        manager.resume(repoID: testRepo)7475        var completed = false76        var lastLogged = -177        while !completed {78            try? await Task.sleep(for: .milliseconds(300))79            guard let t = manager.task(for: testRepo) else { return fail("task vanished") }80            switch t.state {81            case .completed:82                completed = true83            case .failed:84                return fail("resume failed: \(t.errorDescription ?? "?")")85            default:86                let percent = Int(t.fractionCompleted * 100)87                if percent / 20 != lastLogged / 20 {88                    lastLogged = percent89                    log("   … \(percent)%  (\(gb(t.receivedBytes))/\(gb(t.totalBytes)))")90                }91            }92        }93        log("   download completed + size-verified ✅")9495        // 5 — store validation96        log("5) Validating model directory + store scan…")97        store.rescan()98        guard let model = store.model(for: testRepo) else {99            return fail("ModelStore did not recognize the downloaded model")100        }101        log("   \(model.repoID): \(gb(model.sizeBytes)), arch=\(model.architecture ?? "?"), quant=\(model.quantization ?? "?"), ctx=\(model.contextWindow ?? 0), verdict=\(MemoryAdvisor.verdict(weightsBytes: model.sizeBytes).label)")102103        // 6 — delete104        log("6) Deleting…")105        let reclaimed = store.delete(repoID: testRepo)106        guard store.model(for: testRepo) == nil,107            !FileManager.default.fileExists(atPath: root.appendingPathComponent(testRepo).path)108        else { return fail("delete left files behind") }109        log("   reclaimed \(gb(reclaimed)) ✅")110111        log("\nHUB POC: ALL STEPS GREEN")112        exit(0)113    }114115    private static func log(_ text: String) {116        FileHandle.standardError.write(Data((text + "\n").utf8))117    }118119    private static func fail(_ text: String) {120        log("FAIL: \(text)")121        exit(1)122    }123124    private static func gb(_ bytes: Int64) -> String {125        ByteCountFormatter.string(fromByteCount: bytes, countStyle: .file)126    }127128    private static func pct(_ fraction: Double) -> String {129        "\(Int(fraction * 100))%"130    }131}132