// // AIModel.swift // Zyquo Cloud // // Author: Simon-Pierre Boucher // Mail: contact@spboucher.ai // import Foundation /// A chat-capable model offered by a provider. Instances come exclusively from /// `ModelCatalog` (built-in data generated from docs/PROVIDERS.md, dynamic /// `/models` refreshes, and user-defined custom models) — never hardcode these /// in views or clients. struct AIModel: Codable, Identifiable, Hashable { /// Exact model ID as sent in API requests (e.g. "gpt-5.6-terra"). let id: String let provider: ProviderID /// Human-friendly name shown in the UI (e.g. "GPT-5.6 Terra"). let displayName: String /// Context window in tokens. let contextWindow: Int /// Maximum output tokens, when documented. let maxOutputTokens: Int? let capabilities: ModelCapabilities let pricing: ModelPricing? let parameterSupport: ParameterSupport /// Deprecated or superseded models stay selectable but are ranked last and badged. var isLegacy: Bool = false /// Featured/flagship models surface at the top of pickers. var isRecommended: Bool = false /// Base URL override for user-defined custom models; nil for built-ins. var customBaseURL: URL? = nil /// Short badge text for the model chip (e.g. "1M ctx"). var contextBadge: String { switch contextWindow { case 1_000_000...: return "\(contextWindow / 1_000_000)M ctx" case 1_000...: return "\(contextWindow / 1_000)K ctx" default: return "\(contextWindow) ctx" } } } /// What a model can do. Drives UI affordances (attach button, thinking section…) /// and request construction. struct ModelCapabilities: Codable, Hashable { /// Accepts image input. var vision: Bool = false /// Supports function calling / tools. var tools: Bool = false /// Produces reasoning/thinking output (shown in the collapsible section). var reasoning: Bool = false /// Supports SSE streaming (true for every catalog model; custom endpoints may vary). var streaming: Bool = true /// Supports JSON mode / structured output. var jsonMode: Bool = false /// Returns web-search citations (Perplexity sonar family). var citations: Bool = false } /// USD per 1M tokens. Cached/tiered pricing is intentionally simplified to the /// base rate — cost figures in the UI are labeled as estimates. struct ModelPricing: Codable, Hashable { var inputPerMTok: Double var outputPerMTok: Double /// Estimated cost in USD for a usage record. func cost(inputTokens: Int, outputTokens: Int) -> Double { (Double(inputTokens) * inputPerMTok + Double(outputTokens) * outputPerMTok) / 1_000_000 } } /// Which sampling/control parameters a model accepts. Providers reject requests /// carrying unsupported parameters, so requests only include what's supported — /// and Settings only shows sliders that apply. struct ParameterSupport: Codable, Hashable { var temperature: Bool = true var topP: Bool = true var frequencyPenalty: Bool = false var presencePenalty: Bool = false /// Send "max_completion_tokens" instead of "max_tokens" (OpenAI reasoning models, Cerebras). var usesMaxCompletionTokens: Bool = false /// Accepts `reasoning_effort` (OpenAI, xAI, Mistral, DeepSeek, Kimi K-series, Cerebras…). var reasoningEffort: Bool = false /// Anthropic `thinking` / Qwen `enable_thinking` style explicit thinking toggle. var thinkingToggle: Bool = false /// Model rejects non-streaming calls (Qwen qwq/qvq, DashScope-hosted models /// on Together…) — `complete` aggregates a stream instead. var requiresStreaming: Bool = false static let openAIDefault = ParameterSupport(frequencyPenalty: true, presencePenalty: true) } /// Token usage reported by a provider for one exchange. struct TokenUsage: Codable, Hashable { var inputTokens: Int = 0 var outputTokens: Int = 0 var reasoningTokens: Int? = nil var totalTokens: Int { inputTokens + outputTokens } static func + (lhs: TokenUsage, rhs: TokenUsage) -> TokenUsage { TokenUsage( inputTokens: lhs.inputTokens + rhs.inputTokens, outputTokens: lhs.outputTokens + rhs.outputTokens, reasoningTokens: (lhs.reasoningTokens ?? 0) + (rhs.reasoningTokens ?? 0) == 0 ? nil : (lhs.reasoningTokens ?? 0) + (rhs.reasoningTokens ?? 0) ) } }