// Author: Simon-Pierre Boucher // Contact: contact@spboucher.ai // Project: chat.spboucher.ai /** Normalized model definition, decoupled from the OpenRouter wire format. */ export interface ModelDefinition { id: string; // exact OpenRouter model ID — opaque, sent back verbatim name: string; provider?: string; description?: string; contextLength?: number; pricing?: { prompt?: number; // USD per token completion?: number; image?: number; request?: number; }; capabilities: { text: boolean; vision: boolean; reasoning: boolean; tools: boolean; structuredOutput: boolean; }; metadata: { architecture?: string; tokenizer?: string; createdAt?: number; raw?: unknown; }; } /** Normalized application stream events — the browser never sees raw OpenRouter frames. */ export type ChatStreamEvent = | { type: "generation.start"; generationId: string; model: string } | { type: "content.delta"; text: string } | { type: "reasoning.delta"; text: string } | { type: "tool.start"; toolCallId: string; name: string } | { type: "tool.delta"; toolCallId: string; argumentsDelta: string } | { type: "usage"; promptTokens?: number; completionTokens?: number; reasoningTokens?: number; cachedTokens?: number; totalTokens?: number; cost?: number; } | { type: "generation.end" } | { type: "generation.error"; message: string; retryable: boolean }; export interface ChatMessageInput { role: "system" | "user" | "assistant"; content: string; } export interface GenerationRequest { model: string; messages: ChatMessageInput[]; signal?: AbortSignal; temperature?: number; maxTokens?: number; /** Reserved for future per-generation routing config (provider prefs, fallbacks). */ routing?: Record; } /** Narrow gateway abstraction so the frontend never becomes inseparable from OpenRouter. */ export interface ModelGateway { stream(request: GenerationRequest): AsyncIterable; listModels(): Promise; }