Skip to content

Commit c4047a5

Browse files
committed
feat: add friendlyId to LlmModel, TRQL llm_usage integration, and seed-on-startup
- Add friendly_id column to llm_models (llm_model_xxx format) - Use friendlyId as matchedModelId in all external surfaces - Add durationNs render type to TSQLResultsTable and QueryResultsChart - Add 4 example queries for llm_usage in query editor - Add LLM_PRICING_SEED_ON_STARTUP env var for local bootstrapping - Update admin API and seed to generate friendlyId refs TRI-7773
1 parent fbb7738 commit c4047a5

File tree

15 files changed

+130
-13
lines changed

15 files changed

+130
-13
lines changed

apps/webapp/app/components/code/QueryResultsChart.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,11 @@ function createYAxisFormatter(
12091209
formatDurationMilliseconds(value * 1000, { style: "short" });
12101210
}
12111211

1212+
if (format === "durationNs") {
1213+
return (value: number): string =>
1214+
formatDurationMilliseconds(value / 1_000_000, { style: "short" });
1215+
}
1216+
12121217
if (format === "costInDollars" || format === "cost") {
12131218
return (value: number): string => {
12141219
const dollars = format === "cost" ? value / 100 : value;

apps/webapp/app/components/code/TSQLResultsTable.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,11 @@ function getFormattedValue(value: unknown, column: OutputColumnMetadata): string
8181
return formatDurationMilliseconds(value * 1000, { style: "short" });
8282
}
8383
break;
84+
case "durationNs":
85+
if (typeof value === "number") {
86+
return formatDurationMilliseconds(value / 1_000_000, { style: "short" });
87+
}
88+
break;
8489
case "cost":
8590
if (typeof value === "number") {
8691
return formatCurrencyAccurate(value / 100);
@@ -282,6 +287,12 @@ function getDisplayLength(value: unknown, column: OutputColumnMetadata): number
282287
return formatted.length;
283288
}
284289
return 10;
290+
case "durationNs":
291+
if (typeof value === "number") {
292+
const formatted = formatDurationMilliseconds(value / 1_000_000, { style: "short" });
293+
return formatted.length;
294+
}
295+
return 10;
285296
case "cost":
286297
case "costInDollars":
287298
// Currency format: "$1,234.56"
@@ -598,6 +609,15 @@ function CellValue({
598609
);
599610
}
600611
return <span>{String(value)}</span>;
612+
case "durationNs":
613+
if (typeof value === "number") {
614+
return (
615+
<span className="tabular-nums">
616+
{formatDurationMilliseconds(value / 1_000_000, { style: "short" })}
617+
</span>
618+
);
619+
}
620+
return <span>{String(value)}</span>;
601621
case "cost":
602622
if (typeof value === "number") {
603623
return <span className="tabular-nums">{formatCurrencyAccurate(value / 100)}</span>;

apps/webapp/app/env.server.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,7 @@ const EnvironmentSchema = z
12801280
// LLM cost tracking
12811281
LLM_COST_TRACKING_ENABLED: BoolEnv.default(true),
12821282
LLM_PRICING_RELOAD_INTERVAL_MS: z.coerce.number().int().default(5 * 60 * 1000), // 5 minutes
1283+
LLM_PRICING_SEED_ON_STARTUP: BoolEnv.default(false),
12831284

12841285
// Bootstrap
12851286
TRIGGER_BOOTSTRAP_ENABLED: z.string().default("0"),

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/ExamplesContent.tsx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,63 @@ LIMIT 100`,
118118
scope: "environment",
119119
table: "metrics",
120120
},
121+
{
122+
title: "LLM cost by model (past 7d)",
123+
description: "Total cost, input tokens, and output tokens grouped by model over the last 7 days.",
124+
query: `SELECT
125+
response_model,
126+
SUM(total_cost) AS total_cost,
127+
SUM(input_tokens) AS input_tokens,
128+
SUM(output_tokens) AS output_tokens
129+
FROM llm_usage
130+
WHERE start_time > now() - INTERVAL 7 DAY
131+
GROUP BY response_model
132+
ORDER BY total_cost DESC`,
133+
scope: "environment",
134+
table: "llm_usage",
135+
},
136+
{
137+
title: "LLM cost over time",
138+
description: "Total LLM cost bucketed over time. The bucket size adjusts automatically.",
139+
query: `SELECT
140+
timeBucket(),
141+
SUM(total_cost) AS total_cost
142+
FROM llm_usage
143+
GROUP BY timeBucket
144+
ORDER BY timeBucket
145+
LIMIT 1000`,
146+
scope: "environment",
147+
table: "llm_usage",
148+
},
149+
{
150+
title: "Most expensive runs by LLM cost (top 50)",
151+
description: "Top 50 runs by total LLM cost with token breakdown.",
152+
query: `SELECT
153+
run_id,
154+
task_identifier,
155+
SUM(total_cost) AS llm_cost,
156+
SUM(input_tokens) AS input_tokens,
157+
SUM(output_tokens) AS output_tokens
158+
FROM llm_usage
159+
GROUP BY run_id, task_identifier
160+
ORDER BY llm_cost DESC
161+
LIMIT 50`,
162+
scope: "environment",
163+
table: "llm_usage",
164+
},
165+
{
166+
title: "LLM calls by provider",
167+
description: "Count and cost of LLM calls grouped by AI provider.",
168+
query: `SELECT
169+
gen_ai_system,
170+
count() AS call_count,
171+
SUM(total_cost) AS total_cost
172+
FROM llm_usage
173+
GROUP BY gen_ai_system
174+
ORDER BY total_cost DESC`,
175+
scope: "environment",
176+
table: "llm_usage",
177+
},
121178
];
122179

123180
const tableOptions = querySchemas.map((s) => ({ label: s.name, value: s.name }));

apps/webapp/app/routes/admin.api.v1.llm-models.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { type ActionFunctionArgs, type LoaderFunctionArgs, json } from "@remix-r
22
import { z } from "zod";
33
import { prisma } from "~/db.server";
44
import { authenticateApiRequestWithPersonalAccessToken } from "~/services/personalAccessToken.server";
5+
import { generateFriendlyId } from "~/v3/friendlyIdentifiers";
56

67
async function requireAdmin(request: Request) {
78
const authResult = await authenticateApiRequestWithPersonalAccessToken(request);
@@ -93,6 +94,7 @@ export async function action({ request }: ActionFunctionArgs) {
9394
// Create model first, then tiers with explicit model connection
9495
const model = await prisma.llmModel.create({
9596
data: {
97+
friendlyId: generateFriendlyId("llm_model"),
9698
modelName,
9799
matchPattern,
98100
startDate: startDate ? new Date(startDate) : null,

apps/webapp/app/v3/llmPricingRegistry.server.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1-
import { ModelPricingRegistry } from "@internal/llm-pricing";
1+
import { ModelPricingRegistry, seedLlmPricing } from "@internal/llm-pricing";
22
import { trail } from "agentcrumbs"; // @crumbs
3-
import { $replica } from "~/db.server";
3+
import { prisma, $replica } from "~/db.server";
44
import { env } from "~/env.server";
55
import { singleton } from "~/utils/singleton";
66
import { setLlmPricingRegistry } from "./utils/enrichCreatableEvents.server";
77

88
const crumb = trail("webapp:llm-registry"); // @crumbs
99

10+
async function initRegistry(registry: ModelPricingRegistry) {
11+
if (env.LLM_PRICING_SEED_ON_STARTUP) {
12+
crumb("seeding llm pricing on startup"); // @crumbs
13+
const result = await seedLlmPricing(prisma);
14+
crumb("seed complete", { modelsCreated: result.modelsCreated, modelsSkipped: result.modelsSkipped }); // @crumbs
15+
}
16+
17+
await registry.loadFromDatabase();
18+
crumb("registry loaded successfully", { isLoaded: registry.isLoaded }); // @crumbs
19+
}
20+
1021
export const llmPricingRegistry = singleton("llmPricingRegistry", () => {
1122
if (!env.LLM_COST_TRACKING_ENABLED) {
1223
crumb("llm cost tracking disabled via env"); // @crumbs
@@ -19,15 +30,10 @@ export const llmPricingRegistry = singleton("llmPricingRegistry", () => {
1930
// Wire up the registry so enrichCreatableEvents can use it
2031
setLlmPricingRegistry(registry);
2132

22-
registry
23-
.loadFromDatabase()
24-
.then(() => {
25-
crumb("registry loaded successfully", { isLoaded: registry.isLoaded }); // @crumbs
26-
})
27-
.catch((err) => {
28-
crumb("registry load failed", { error: String(err) }); // @crumbs
29-
console.error("Failed to load LLM pricing registry", err);
30-
});
33+
initRegistry(registry).catch((err) => {
34+
crumb("registry init failed", { error: String(err) }); // @crumbs
35+
console.error("Failed to initialize LLM pricing registry", err);
36+
});
3137

3238
// Periodic reload
3339
const reloadInterval = env.LLM_PRICING_RELOAD_INTERVAL_MS;

apps/webapp/test/otlpExporter.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ describe("OTLPExporter", () => {
406406
const inputCost = (usageDetails["input"] ?? 0) * 0.0000025;
407407
const outputCost = (usageDetails["output"] ?? 0) * 0.00001;
408408
return {
409-
matchedModelId: "model-gpt4o",
409+
matchedModelId: "llm_model_gpt4o",
410410
matchedModelName: "gpt-4o",
411411
pricingTierId: "tier-standard",
412412
pricingTierName: "Standard",

internal-packages/database/prisma/migrations/20260310155049_add_llm_pricing_tables/migration.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
-- CreateTable
22
CREATE TABLE "public"."llm_models" (
33
"id" TEXT NOT NULL,
4+
"friendly_id" TEXT NOT NULL,
45
"project_id" TEXT,
56
"model_name" TEXT NOT NULL,
67
"match_pattern" TEXT NOT NULL,
@@ -35,6 +36,9 @@ CREATE TABLE "public"."llm_prices" (
3536
CONSTRAINT "llm_prices_pkey" PRIMARY KEY ("id")
3637
);
3738

39+
-- CreateIndex
40+
CREATE UNIQUE INDEX "llm_models_friendly_id_key" ON "public"."llm_models"("friendly_id");
41+
3842
-- CreateIndex
3943
CREATE INDEX "llm_models_project_id_idx" ON "public"."llm_models"("project_id");
4044

internal-packages/database/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2586,6 +2586,7 @@ model MetricsDashboard {
25862586
/// A known LLM model or model pattern for cost tracking
25872587
model LlmModel {
25882588
id String @id @default(cuid())
2589+
friendlyId String @unique @map("friendly_id")
25892590
projectId String? @map("project_id")
25902591
project Project? @relation(fields: [projectId], references: [id], onDelete: Cascade)
25912592
modelName String @map("model_name")

internal-packages/llm-pricing/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"types": "./src/index.ts",
77
"type": "module",
88
"dependencies": {
9+
"@trigger.dev/core": "workspace:*",
910
"@trigger.dev/database": "workspace:*"
1011
},
1112
"scripts": {

0 commit comments

Comments
 (0)