Files
triggerdotdev--trigger.dev/apps/webapp/app/utils/columnFormat.ts
Eric Allam 1cfc296c6b feat(ai): LLM metrics tracking and AI span inspector (#3213)
- Automatic LLM cost enrichment for AI SDK spans (streamText,
generateText, generateObject) or any other spans that use semantic
gen_ai attributes with support for 145+ models
- New AI span inspector sidebar showing model, tokens, cost, messages,
tool calls, and response text
- LLM metrics dual-write to ClickHouse `llm_metrics_v1` table for
analytics
- LLM metrics built-in dashboard (unlinked at the moment)
- Provider cost fallback — uses gateway/OpenRouter reported costs from
`providerMetadata` when registry pricing is unavailable
- Prefix-stripping for gateway/OpenRouter model names (e.g.
`mistral/mistral-large-3` matches `mistral-large-3` pricing)
- Admin dashboard for managing LLM model pricing (list, create, edit,
delete, search, test pattern matching)
- Missing models detection page — queries ClickHouse for unpriced models
with sample spans and Claude Code-ready prompts for adding pricing
- AI span seed script (`pnpm run db:seed:ai-spans`) with 51 spans across
12 provider systems for local dev testing
- UI fixes: `completionTokens`/`promptTokens` aliases,
`ai.response.object` display for generateObject, cache read/write token
breakdown

## Screenshots:

<img width="1030" height="104" alt="CleanShot 2026-03-17 at 16 48 54@2x"
src="https://github.com/user-attachments/assets/bc8fccda-e48b-4d0c-bfb1-e620064e5979"
/>

<img width="1094" height="1512" alt="CleanShot 2026-03-17 at 16 49
23@2x"
src="https://github.com/user-attachments/assets/c2424569-d07e-4d67-a436-e8250043a1ee"
/>

<img width="1074" height="1412" alt="CleanShot 2026-03-17 at 16 49
18@2x"
src="https://github.com/user-attachments/assets/22342ac4-4769-45d1-a328-a24fb9a82a50"
/>

<img width="1012" height="2292" alt="CleanShot 2026-03-17 at 16 39
01@2x"
src="https://github.com/user-attachments/assets/59e327d1-6652-4293-8be0-bb8326e5fbc5"
/>

<img width="3680" height="2392" alt="CleanShot 2026-03-15 at 08 29
38@2x"
src="https://github.com/user-attachments/assets/1f77beb8-de67-495b-b890-bcdb8d7f1fe8"
/>

---------

Co-authored-by: James Ritchie <james@trigger.dev>
2026-03-17 18:26:43 +00:00

84 lines
2.7 KiB
TypeScript

import type { ColumnFormatType } from "@internal/clickhouse";
import { formatDurationMilliseconds } from "@trigger.dev/core/v3";
import { formatCurrencyAccurate } from "~/utils/numberFormatter";
/**
* Format a number as binary bytes (KiB, MiB, GiB, TiB)
*/
export function formatBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
const i = Math.min(
Math.max(0, Math.floor(Math.log(Math.abs(bytes)) / Math.log(1024))),
units.length - 1
);
return `${(bytes / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
}
/**
* Format a number as decimal bytes (KB, MB, GB, TB)
*/
export function formatDecimalBytes(bytes: number): string {
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
const i = Math.min(
Math.max(0, Math.floor(Math.log(Math.abs(bytes)) / Math.log(1000))),
units.length - 1
);
return `${(bytes / Math.pow(1000, i)).toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
}
/**
* Format a large number with human-readable suffix (K, M, B)
*/
export function formatQuantity(value: number): string {
const abs = Math.abs(value);
if (abs >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(2)}B`;
if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(2)}M`;
if (abs >= 1_000) return `${(value / 1_000).toFixed(2)}K`;
return value.toLocaleString();
}
/**
* Format a dollar amount with adaptive precision — avoids trailing zeros.
*/
function formatCostAdaptive(dollars: number): string {
if (dollars === 0) return "$0";
const abs = Math.abs(dollars);
if (abs >= 1000) return `$${dollars.toFixed(2)}`;
if (abs >= 1) return `$${dollars.toFixed(2)}`;
if (abs >= 0.01) return `$${dollars.toFixed(4)}`;
if (abs >= 0.0001) return `$${dollars.toFixed(6)}`;
return formatCurrencyAccurate(dollars);
}
/**
* Creates a value formatter function for a given column format type.
* Used by chart tooltips, legend values, and big number cards.
*/
export function createValueFormatter(
format?: ColumnFormatType
): ((value: number) => string) | undefined {
if (!format) return undefined;
switch (format) {
case "bytes":
return (v) => formatBytes(v);
case "decimalBytes":
return (v) => formatDecimalBytes(v);
case "percent":
return (v) => `${v.toFixed(2)}%`;
case "quantity":
return (v) => formatQuantity(v);
case "duration":
return (v) => formatDurationMilliseconds(v, { style: "short" });
case "durationSeconds":
return (v) => formatDurationMilliseconds(v * 1000, { style: "short" });
case "costInDollars":
return (v) => formatCostAdaptive(v);
case "cost":
return (v) => formatCostAdaptive(v / 100);
default:
return undefined;
}
}