1cfc296c6b
- 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>
84 lines
2.7 KiB
TypeScript
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;
|
|
}
|
|
}
|