spb/chat-spboucher Public
Private universal chat interface over the OpenRouter ecosystem — 400+ models, branching, streaming, usage tracking. Next.js 16 + SQLite, PWA, deployed on m4m64a at chat.spboucher.ai
TypeScript 78.8%
CSS 15.1%
JavaScript 4.9%
Shell 1.2%
1// Author: Simon-Pierre Boucher2// Contact: contact@spboucher.ai3// Project: chat.spboucher.ai45// Context compiler: DB history and model context are separate concepts.6// Compiles the active thread into a request that fits the model's window.78import type { ChatMessageInput } from "@/lib/openrouter";9import type { MessageRow } from "@/lib/conversations";1011/** Cheap token estimate: ~4 characters per token. Deliberately conservative. */12export function estimateTokens(text: string): number {13 return Math.ceil(text.length / 3.6);14}1516export interface CompiledContext {17 messages: ChatMessageInput[];18 estimatedPromptTokens: number;19 trimmedCount: number;20}2122const RESERVED_OUTPUT_TOKENS = 4096;2324/**25 * Compile the thread for a request: keep system instructions, keep the most26 * recent messages, trim oldest low-value context when over budget.27 */28export function compileContext(29 thread: MessageRow[],30 contextLength: number | undefined,31 systemPrompt?: string32): CompiledContext {33 const budget = Math.max((contextLength ?? 128_000) - RESERVED_OUTPUT_TOKENS, 8_000);3435 const system: ChatMessageInput[] = systemPrompt36 ? [{ role: "system", content: systemPrompt }]37 : [];38 const systemTokens = systemPrompt ? estimateTokens(systemPrompt) : 0;3940 // Only completed / meaningful messages reach the model.41 const usable = thread.filter(42 (m) => (m.role === "user" || m.role === "assistant") && m.content.length > 043 );4445 // Walk from newest to oldest, accumulating until the budget is spent.46 const kept: MessageRow[] = [];47 let used = systemTokens;48 for (let i = usable.length - 1; i >= 0; i--) {49 const t = estimateTokens(usable[i].content) + 6; // per-message overhead50 if (used + t > budget && kept.length > 0) break;51 kept.push(usable[i]);52 used += t;53 }54 kept.reverse();5556 return {57 messages: [58 ...system,59 ...kept.map((m): ChatMessageInput => ({ role: m.role as "user" | "assistant", content: m.content })),60 ],61 estimatedPromptTokens: used,62 trimmedCount: usable.length - kept.length,63 };64}65