TypeScript 97.4%
SQL 1%
JavaScript 0.9%
CSS 0.6%
1import type { PolyModel } from "@/lib/client/types";23/**4 * Client-side token & cost estimation. Heuristic (≈ 4 chars/token for English prose, ~3.2 for code)5 * — precise enough for "≈ $0.08" hints; the server records exact provider usage afterwards.6 */7export function estimateTextTokens(text: string): number {8 if (!text) return 0;9 const codeish = /[{}();=<>[\]]/.test(text) && (text.match(/\n/g)?.length ?? 0) > 2;10 const perToken = codeish ? 3.2 : 4.0;11 // CJK scripts are ~1 token per character.12 const cjk = (text.match(/[-ヿ㐀-䶿一-鿿가-]/g) ?? []).length;13 return Math.ceil((text.length - cjk) / perToken + cjk);14}1516export interface AttachmentLike {17 kind: string;18 sizeBytes: number;19 width?: number | null;20 height?: number | null;21}2223/** Rough per-attachment token cost (images follow the common 512px-tile heuristic; text ≈ bytes / 4; PDFs ≈ 1.5K / page ≈ bytes/45). */24export function estimateAttachmentTokens(a: AttachmentLike): number {25 if (a.kind === "image") {26 const w = a.width ?? 1024;27 const h = a.height ?? 1024;28 const tiles = Math.ceil(Math.min(w, 2048) / 512) * Math.ceil(Math.min(h, 2048) / 512);29 return 85 + tiles * 170;30 }31 if (a.kind === "pdf") return Math.max(300, Math.ceil(a.sizeBytes / 45));32 return Math.ceil(a.sizeBytes / 4);33}3435export interface ContextEstimate {36 /** Tokens in the history that will be replayed. */37 history: number;38 /** Tokens in the draft + attachments + system prompt. */39 draft: number;40 total: number;41 contextTokens: number | null;42 /** 0..1 or null when the model has no known limit. */43 ratio: number | null;44 level: "ok" | "warn" | "critical" | "over";45}4647export function estimateContext(opts: { historyText: string[]; historyAttachments?: AttachmentLike[]; draft: string; attachments?: AttachmentLike[]; systemPrompt?: string | null; model?: PolyModel | null; expectedOutput?: number }): ContextEstimate {48 const history = opts.historyText.reduce((n, t) => n + estimateTextTokens(t), 0) + (opts.historyAttachments ?? []).reduce((n, a) => n + estimateAttachmentTokens(a), 0) + opts.historyText.length * 4;49 const draft = estimateTextTokens(opts.draft) + (opts.attachments ?? []).reduce((n, a) => n + estimateAttachmentTokens(a), 0) + estimateTextTokens(opts.systemPrompt ?? "");50 const total = history + draft;51 const contextTokens = opts.model?.limits?.contextTokens ?? null;52 const ratio = contextTokens ? (total + (opts.expectedOutput ?? 0)) / contextTokens : null;53 const level = ratio === null ? "ok" : ratio >= 1 ? "over" : ratio >= 0.95 ? "critical" : ratio >= 0.8 ? "warn" : "ok";54 return { history, draft, total, contextTokens, ratio, level };55}5657export interface CostEstimate {58 inputTokens: number;59 outputTokens: number;60 /** null when the model has no public pricing. */61 usd: number | null;62 inputUsd: number | null;63 outputUsd: number | null;64}6566/** Estimate a request cost for a model: input tokens known, output tokens assumed (default 600, or maxTokens when smaller). */67export function estimateCost(model: PolyModel | null | undefined, inputTokens: number, outputTokens = 600): CostEstimate {68 const p = model?.pricing;69 if (!p || (p.inputPerMillion === undefined && p.outputPerMillion === undefined)) return { inputTokens, outputTokens, usd: null, inputUsd: null, outputUsd: null };70 let inRate = p.inputPerMillion ?? 0;71 let outRate = p.outputPerMillion ?? 0;72 if (p.longContext && inputTokens > p.longContext.thresholdTokens) {73 inRate = p.longContext.inputPerMillion ?? inRate;74 outRate = p.longContext.outputPerMillion ?? outRate;75 }76 const inputUsd = (inputTokens / 1_000_000) * inRate;77 const outputUsd = (outputTokens / 1_000_000) * outRate;78 return { inputTokens, outputTokens, usd: inputUsd + outputUsd, inputUsd, outputUsd };79}8081/** Compact "~22K tokens · ≈ $0.08" label. */82export function formatEstimate(tokens: number, usd: number | null): string {83 const t = tokens >= 1_000_000 ? `${(tokens / 1_000_000).toFixed(1)}M` : tokens >= 1000 ? `${Math.round(tokens / 1000)}K` : String(tokens);84 if (usd === null) return `~${t} tokens`;85 const u = usd < 0.01 ? `$${usd.toFixed(4)}` : `$${usd.toFixed(2)}`;86 return `~${t} tokens · ≈ ${u}`;87}8889/** Above this estimated cost the composer asks for confirmation before sending. */90export const COST_CONFIRM_THRESHOLD_USD = 1.0;91