format.ts
Language TypeScript2.9 KB86 lines
// Money arrives as whole nano-dollars (1e-9 USD); tokens as integers.import type { ModelInfo } from '../ipc/gen/ModelInfo'import type { Prices } from '../ipc/gen/Prices'export const NANO = 1e9export function usd(nano: number): string {if (nano === 0) return '$0'const v = nano / NANOif (v < 0.0001) return '<$0.0001'if (v < 1) return `$${v.toFixed(4)}`return `$${v.toFixed(2)}`}/** Price per million tokens: "$0.50", "$0.075", "free". */export function perMillion(p: number): string {if (p === 0) return 'free'if (p >= 1) return `$${p.toFixed(2)}`const s = p.toFixed(3)return `$${s.endsWith('0') ? p.toFixed(2) : s}`}/** "free", or input and output per million tokens: "$0.09 / $0.55". */export function priceLabel(p: Prices): string {return p.input_per_m === 0 && p.output_per_m === 0? 'free': `${perMillion(p.input_per_m)} / ${perMillion(p.output_per_m)}`}/** Can the model read images? DeepInfra tags these `vision`, some also `vlm`. Mirrors `ModelInfo::vision` in Rust. */export function isVision(m: ModelInfo): boolean {return m.tags.includes('vision') || m.tags.includes('vlm')}/** File names Cortex treats as images; `IMAGE_EXTENSIONS` in attachments.rs is the Rust list. */export const IMAGE_EXT = /\.(png|jpe?g|gif|webp)$/iexport function tokens(n: number): string {if (n < 1000) return String(n)if (n < 10_000) return `${(n / 1000).toFixed(1)}k`if (n < 1_000_000) return `${Math.round(n / 1000)}k`return `${(n / 1_000_000).toFixed(n < 10_000_000 ? 1 : 0)}M`}export function thousands(n: number): string {return n.toLocaleString('en-US')}export function seconds(ms: number | null | undefined): string {if (ms == null) return ''return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`}export function relativeDay(ms: number, now = Date.now()): string {const d = new Date(ms)const today = new Date(now)today.setHours(0, 0, 0, 0)const diff = Math.floor((today.getTime() - new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()) / 86_400_000,)if (diff <= 0) return 'Today'if (diff === 1) return 'Yesterday'if (diff < 7) return 'This week'if (diff < 31) return 'This month'return 'Earlier'}/** The short name of a model id: "Qwen/Qwen3-235B-A22B" -> "Qwen3-235B-A22B". */export function shortModel(id: string): string {const i = id.lastIndexOf('/')return i >= 0 ? id.slice(i + 1) : id}/** Search snippets mark hits with \u0001 ... \u0002. */export function snippetParts(s: string): { text: string; hit: boolean }[] {const out: { text: string; hit: boolean }[] = []const re = /\u0001([^\u0002]*)\u0002/glet last = 0for (let m = re.exec(s); m; m = re.exec(s)) {if (m.index > last) out.push({ text: s.slice(last, m.index), hit: false })out.push({ text: m[1], hit: true })last = m.index + m[0].length}if (last < s.length) out.push({ text: s.slice(last), hit: false })return out}