SPB Git forge

spb/polyllm

Public
15commits 1branches 0releases
2.2 MBsize
maindefault branch
13 days agolast push
TypeScript 97.4% SQL 1% JavaScript 0.9% CSS 0.6%
4.3 KB · 91 lines typescript
Raw Blame History
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