/* * index.ts (types) * Zyquo Cloud Web * * Author: Simon-Pierre Boucher * Mail: contact@spboucher.ai * * Core data model, ported 1:1 from native Zyquo Cloud * (ProviderID.swift, AIModel.swift, Message.swift, Conversation.swift). */ /** The 12 built-in cloud AI providers, plus user-defined custom endpoints. */ export type Provider = | 'openai' | 'anthropic' | 'xai' | 'mistral' | 'gemini' | 'qwen' | 'deepseek' | 'kimi' | 'perplexity' | 'together' | 'deepinfra' | 'cerebras' | 'custom' export const BUILT_IN_PROVIDERS: readonly Provider[] = [ 'openai', 'anthropic', 'xai', 'mistral', 'gemini', 'qwen', 'deepseek', 'kimi', 'perplexity', 'together', 'deepinfra', 'cerebras', ] /** The request/response schema a provider speaks. */ export type WireFormat = 'openAIChatCompletions' | 'anthropicMessages' /** What a model can do. Drives UI affordances and request construction. */ export interface ModelCapabilities { vision: boolean tools: boolean reasoning: boolean streaming: boolean jsonMode: boolean citations: boolean } /** USD per 1M tokens (base rate — UI labels costs as estimates). */ export interface ModelPricing { inputPerMTok: number outputPerMTok: number } /** Estimated cost in USD for a usage record. */ export function pricingCost(pricing: ModelPricing, inputTokens: number, outputTokens: number): number { return (inputTokens * pricing.inputPerMTok + outputTokens * pricing.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 the params panel only shows controls that apply. */ export interface ParameterSupport { temperature: boolean topP: boolean frequencyPenalty: boolean presencePenalty: boolean /** Send "max_completion_tokens" instead of "max_tokens". */ usesMaxCompletionTokens: boolean /** Accepts `reasoning_effort`. */ reasoningEffort: boolean /** Anthropic `thinking` / Qwen `enable_thinking` explicit toggle. */ thinkingToggle: boolean /** Model rejects non-streaming calls — `complete` aggregates a stream. */ requiresStreaming: boolean } /** A chat-capable model offered by a provider. Instances come from the catalog. */ export interface AIModel { /** Exact model ID as sent in API requests. */ id: string provider: Provider displayName: string contextWindow: number maxOutputTokens: number | null capabilities: ModelCapabilities pricing: ModelPricing | null parameterSupport: ParameterSupport isLegacy: boolean isRecommended: boolean /** Base URL override for user-defined custom models; undefined for built-ins. */ customBaseURL?: string } /** Short badge text for the model chip (e.g. "1M ctx"). */ export function contextBadge(model: AIModel): string { const ctx = model.contextWindow if (ctx >= 1_000_000) return `${Math.floor(ctx / 1_000_000)}M ctx` if (ctx >= 1_000) return `${Math.floor(ctx / 1_000)}K ctx` return `${ctx} ctx` } /** Token usage reported by a provider for one exchange. */ export interface TokenUsage { inputTokens: number outputTokens: number reasoningTokens?: number } export function addUsage(a: TokenUsage, b: TokenUsage): TokenUsage { const reasoning = (a.reasoningTokens ?? 0) + (b.reasoningTokens ?? 0) return { inputTokens: a.inputTokens + b.inputTokens, outputTokens: a.outputTokens + b.outputTokens, ...(reasoning > 0 ? { reasoningTokens: reasoning } : {}), } } /** One numbered web citation (Perplexity sonar family). */ export interface Citation { index: number url: string title?: string } export type AttachmentKind = 'image' | 'textFile' /** An image or text-file attachment on a user message. */ export interface Attachment { id: string kind: AttachmentKind fileName: string mimeType: string /** Base64 payload (images) or UTF-8 text content (text files). */ data: string } export type MessageRole = 'user' | 'assistant' | 'system' /** One chat turn. */ export interface Message { id: string role: MessageRole text: string reasoning?: string citations?: Citation[] attachments?: Attachment[] modelID?: string provider?: Provider usage?: TokenUsage estimatedCost?: number errorText?: string createdAt: number /** Alternate assistant responses (regenerate variants); active one is `text`. */ variants?: MessageVariant[] /** Index of the active variant in `variants`, if any. */ activeVariant?: number pinned?: boolean note?: string } /** A stored alternate response for one assistant turn. */ export interface MessageVariant { text: string reasoning?: string citations?: Citation[] modelID?: string provider?: Provider usage?: TokenUsage estimatedCost?: number createdAt: number } /** Per-conversation sampling parameters. undefined = provider default (omitted). */ export interface ChatParameters { temperature?: number topP?: number maxTokens?: number frequencyPenalty?: number presencePenalty?: number reasoningEffort?: 'low' | 'medium' | 'high' thinkingEnabled?: boolean } /** One conversation thread. */ export interface Conversation { id: string title: string createdAt: number updatedAt: number modelID: string provider: Provider systemPrompt?: string parameters: ChatParameters messages: Message[] pinned: boolean archived?: boolean tags?: string[] personaID?: string /** False once the user manually renames (disables auto-titling). */ hasAutoTitle: boolean /** Conversation this one was branched from, if any. */ branchedFrom?: { conversationID: string; messageID: string } } export type ThemeMode = 'light' | 'dark' | 'system' export type AccentChoice = 'indigo' | 'graphite' | 'teal' | 'amber' | 'rose' export type MessageDensity = 'comfortable' | 'compact' /** Global app settings (storage/settings.ts). */ export interface Settings { theme: ThemeMode accent: AccentChoice chatFontSize: number density: MessageDensity defaultModelID: string defaultProvider: Provider defaultSystemPrompt: string defaultParameters: ChatParameters /** Per-provider base-URL overrides (user proxy / Zyquo Router). */ proxyBaseURLs: Partial> streamingEnabled: boolean /** User-defined model aliases (e.g. "fast" → model). */ aliases: Record favoriteModelIDs: string[] recentModelIDs: string[] firstRunAcknowledged: boolean focusMode: boolean }