mattrguerra/cortexmainPrivate on GitHub, published here in full

format.ts

Language TypeScript2.9 KB86 lines

src/lib/format.ts

  1. // Money arrives as whole nano-dollars (1e-9 USD); tokens as integers.
  2. import type { ModelInfo } from '../ipc/gen/ModelInfo'
  3. import type { Prices } from '../ipc/gen/Prices'
  4. export const NANO = 1e9
  5. export function usd(nano: number): string {
  6. if (nano === 0) return '$0'
  7. const v = nano / NANO
  8. if (v < 0.0001) return '<$0.0001'
  9. if (v < 1) return `$${v.toFixed(4)}`
  10. return `$${v.toFixed(2)}`
  11. }
  12. /** Price per million tokens: "$0.50", "$0.075", "free". */
  13. export function perMillion(p: number): string {
  14. if (p === 0) return 'free'
  15. if (p >= 1) return `$${p.toFixed(2)}`
  16. const s = p.toFixed(3)
  17. return `$${s.endsWith('0') ? p.toFixed(2) : s}`
  18. }
  19. /** "free", or input and output per million tokens: "$0.09 / $0.55". */
  20. export function priceLabel(p: Prices): string {
  21. return p.input_per_m === 0 && p.output_per_m === 0
  22. ? 'free'
  23. : `${perMillion(p.input_per_m)} / ${perMillion(p.output_per_m)}`
  24. }
  25. /** Can the model read images? DeepInfra tags these `vision`, some also `vlm`. Mirrors `ModelInfo::vision` in Rust. */
  26. export function isVision(m: ModelInfo): boolean {
  27. return m.tags.includes('vision') || m.tags.includes('vlm')
  28. }
  29. /** File names Cortex treats as images; `IMAGE_EXTENSIONS` in attachments.rs is the Rust list. */
  30. export const IMAGE_EXT = /\.(png|jpe?g|gif|webp)$/i
  31. export function tokens(n: number): string {
  32. if (n < 1000) return String(n)
  33. if (n < 10_000) return `${(n / 1000).toFixed(1)}k`
  34. if (n < 1_000_000) return `${Math.round(n / 1000)}k`
  35. return `${(n / 1_000_000).toFixed(n < 10_000_000 ? 1 : 0)}M`
  36. }
  37. export function thousands(n: number): string {
  38. return n.toLocaleString('en-US')
  39. }
  40. export function seconds(ms: number | null | undefined): string {
  41. if (ms == null) return ''
  42. return ms < 1000 ? `${ms} ms` : `${(ms / 1000).toFixed(1)} s`
  43. }
  44. export function relativeDay(ms: number, now = Date.now()): string {
  45. const d = new Date(ms)
  46. const today = new Date(now)
  47. today.setHours(0, 0, 0, 0)
  48. const diff = Math.floor(
  49. (today.getTime() - new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()) / 86_400_000,
  50. )
  51. if (diff <= 0) return 'Today'
  52. if (diff === 1) return 'Yesterday'
  53. if (diff < 7) return 'This week'
  54. if (diff < 31) return 'This month'
  55. return 'Earlier'
  56. }
  57. /** The short name of a model id: "Qwen/Qwen3-235B-A22B" -> "Qwen3-235B-A22B". */
  58. export function shortModel(id: string): string {
  59. const i = id.lastIndexOf('/')
  60. return i >= 0 ? id.slice(i + 1) : id
  61. }
  62. /** Search snippets mark hits with \u0001 ... \u0002. */
  63. export function snippetParts(s: string): { text: string; hit: boolean }[] {
  64. const out: { text: string; hit: boolean }[] = []
  65. const re = /\u0001([^\u0002]*)\u0002/g
  66. let last = 0
  67. for (let m = re.exec(s); m; m = re.exec(s)) {
  68. if (m.index > last) out.push({ text: s.slice(last, m.index), hit: false })
  69. out.push({ text: m[1], hit: true })
  70. last = m.index + m[0].length
  71. }
  72. if (last < s.length) out.push({ text: s.slice(last), hit: false })
  73. return out
  74. }