SPB Git

spb/zyquo-cloud Public MIT

Native macOS AI chat client for 12 cloud providers — your keys, every cloud model, one beautiful chat.

Swift 97.4% Shell 1.7% Makefile 1%
3.7 KB · 101 lines swift
Raw Blame History
1//2//  ModelTests.swift3//  Zyquo Cloud4//5//  Author: Simon-Pierre Boucher6//  Mail: contact@spboucher.ai7//89import Foundation10import Testing11@testable import ZyquoCloud1213@Suite struct ModelTests {14    @Test func conversationJSONRoundTrip() throws {15        // ISO8601 persistence has whole-second precision; use round dates.16        let date = Date(timeIntervalSince1970: 1_785_400_000)17        var message = Message(role: .user, text: "Hello", createdAt: date)18        message.attachments = [19            Attachment(kind: .image, fileName: "pic.png", data: Data([1, 2, 3]), mimeType: "image/png")20        ]21        var reply = Message(role: .assistant, text: "Hi!", reasoning: "thinking…", createdAt: date)22        reply.usage = TokenUsage(inputTokens: 10, outputTokens: 5)23        reply.citations = [Citation(index: 1, url: URL(string: "https://example.com")!, title: "Example")]2425        let conversation = Conversation(26            title: "Test",27            messages: [message, reply],28            modelID: "gpt-4o-mini",29            provider: .openai,30            systemPrompt: "Be brief.",31            createdAt: date,32            updatedAt: date33        )3435        let encoder = JSONEncoder()36        encoder.dateEncodingStrategy = .iso860137        let decoder = JSONDecoder()38        decoder.dateDecodingStrategy = .iso860139        let data = try encoder.encode(conversation)40        let decoded = try decoder.decode(Conversation.self, from: data)4142        #expect(decoded == conversation)43        #expect(decoded.totalUsage.totalTokens == 15)44    }4546    @Test func pricingCost() {47        let pricing = ModelPricing(inputPerMTok: 2, outputPerMTok: 10)48        #expect(abs(pricing.cost(inputTokens: 1_000_000, outputTokens: 500_000) - 7.0) < 0.0001)49    }5051    @Test func contextBadge() {52        func model(_ ctx: Int) -> AIModel {53            AIModel(54                id: "m", provider: .openai, displayName: "M", contextWindow: ctx,55                maxOutputTokens: nil, capabilities: ModelCapabilities(),56                pricing: nil, parameterSupport: ParameterSupport()57            )58        }59        #expect(model(1_000_000).contextBadge == "1M ctx")60        #expect(model(128_000).contextBadge == "128K ctx")61    }6263    @Test func providerErrorMapping() {64        let unauthorized = ProviderError.from(status: 401, body: Data(), provider: .mistral)65        guard case .invalidAPIKey(let provider) = unauthorized else {66            Issue.record("expected invalidAPIKey")67            return68        }69        #expect(provider == .mistral)7071        let body = #"{"error":{"message":"model not found"}}"#.data(using: .utf8)!72        let notFound = ProviderError.from(status: 404, body: body, provider: .openai)73        guard case .badRequest(_, let message) = notFound else {74            Issue.record("expected badRequest")75            return76        }77        #expect(message == "model not found")78    }7980    @Test @MainActor func cheapestModelPrefersNonReasoning() {81        let catalog = ModelCatalog()82        for provider in ProviderID.builtIn {83            guard let cheapest = catalog.cheapestModel(for: provider) else { continue }84            let plainExists = catalog.models(for: provider)85                .contains { !$0.isLegacy && !$0.capabilities.reasoning }86            if plainExists {87                #expect(!cheapest.capabilities.reasoning,88                        "\(provider): utility calls must not use a reasoning model")89            }90        }91    }9293    @Test func everyBuiltInProviderHasBaseURLAndFormat() {94        for provider in ProviderID.builtIn {95            #expect(provider.defaultBaseURL != nil, "\(provider) missing base URL")96        }97        #expect(ProviderID.anthropic.wireFormat == .anthropicMessages)98        #expect(ProviderID.builtIn.count == 12)99    }100}101