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>
312 lines
9.3 KiB
TypeScript
312 lines
9.3 KiB
TypeScript
import { prisma } from "./app/db.server";
|
|
import { createOrganization } from "./app/models/organization.server";
|
|
import { createProject } from "./app/models/project.server";
|
|
import { AuthenticationMethod, Organization, Prisma, User } from "@trigger.dev/database";
|
|
|
|
async function seed() {
|
|
console.log("🌱 Starting seed...");
|
|
|
|
// Create or find the local user
|
|
let user = await prisma.user.findUnique({
|
|
where: { email: "local@trigger.dev" },
|
|
});
|
|
|
|
if (!user) {
|
|
console.log("Creating local user...");
|
|
user = await prisma.user.create({
|
|
data: {
|
|
email: "local@trigger.dev",
|
|
authenticationMethod: AuthenticationMethod.MAGIC_LINK,
|
|
name: "Local Developer",
|
|
displayName: "Local Developer",
|
|
admin: true,
|
|
confirmedBasicDetails: true,
|
|
},
|
|
});
|
|
console.log(`✅ Created user: ${user.email} (${user.id})`);
|
|
} else {
|
|
console.log(`✅ User already exists: ${user.email} (${user.id})`);
|
|
}
|
|
|
|
// Create or find the references organization
|
|
// Look for an organization where the user is a member and the title is "References"
|
|
let organization = await prisma.organization.findFirst({
|
|
where: {
|
|
title: "References",
|
|
members: {
|
|
some: {
|
|
userId: user.id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!organization) {
|
|
console.log("Creating references organization...");
|
|
organization = await createOrganization({
|
|
title: "References",
|
|
userId: user.id,
|
|
companySize: "1-10",
|
|
});
|
|
console.log(`✅ Created organization: ${organization.title} (${organization.slug})`);
|
|
} else {
|
|
console.log(`✅ Organization already exists: ${organization.title} (${organization.slug})`);
|
|
}
|
|
|
|
// Define the reference projects with their specific project refs
|
|
const referenceProjects = [
|
|
{
|
|
name: "hello-world",
|
|
externalRef: "proj_rrkpdguyagvsoktglnod",
|
|
},
|
|
{
|
|
name: "d3-chat",
|
|
externalRef: "proj_cdmymsrobxmcgjqzhdkq",
|
|
},
|
|
{
|
|
name: "realtime-streams",
|
|
externalRef: "proj_klxlzjnzxmbgiwuuwhvb",
|
|
},
|
|
];
|
|
|
|
// Create or find each project
|
|
for (const projectConfig of referenceProjects) {
|
|
await findOrCreateProject(projectConfig.name, organization, user.id, projectConfig.externalRef);
|
|
}
|
|
|
|
await createBatchLimitOrgs(user);
|
|
await ensureDefaultWorkerGroup();
|
|
|
|
console.log("\n🎉 Seed complete!\n");
|
|
console.log("Summary:");
|
|
console.log(`User: ${user.email}`);
|
|
console.log(`Organization: ${organization.title} (${organization.slug})`);
|
|
console.log(`Projects: ${referenceProjects.map((p) => p.name).join(", ")}`);
|
|
console.log("\n⚠️ Note: Update the .env files in d3-chat and realtime-streams with:");
|
|
console.log(` - d3-chat: TRIGGER_PROJECT_REF=proj_cdmymsrobxmcgjqzhdkq`);
|
|
console.log(` - realtime-streams: TRIGGER_PROJECT_REF=proj_klxlzjnzxmbgiwuuwhvb`);
|
|
}
|
|
|
|
async function createBatchLimitOrgs(user: User) {
|
|
const org1 = await findOrCreateOrganization("batch-limit-org-1", user, {
|
|
batchQueueConcurrencyConfig: { processingConcurrency: 1 },
|
|
});
|
|
const org2 = await findOrCreateOrganization("batch-limit-org-2", user, {
|
|
batchQueueConcurrencyConfig: { processingConcurrency: 5 },
|
|
});
|
|
const org3 = await findOrCreateOrganization("batch-limit-org-3", user, {
|
|
batchQueueConcurrencyConfig: { processingConcurrency: 10 },
|
|
});
|
|
|
|
// Create 3 projects in each organization
|
|
const org1Project1 = await findOrCreateProject("batch-limit-project-1", org1, user.id);
|
|
const org1Project2 = await findOrCreateProject("batch-limit-project-2", org1, user.id);
|
|
const org1Project3 = await findOrCreateProject("batch-limit-project-3", org1, user.id);
|
|
|
|
const org2Project1 = await findOrCreateProject("batch-limit-project-1", org2, user.id);
|
|
const org2Project2 = await findOrCreateProject("batch-limit-project-2", org2, user.id);
|
|
const org2Project3 = await findOrCreateProject("batch-limit-project-3", org2, user.id);
|
|
|
|
const org3Project1 = await findOrCreateProject("batch-limit-project-1", org3, user.id);
|
|
const org3Project2 = await findOrCreateProject("batch-limit-project-2", org3, user.id);
|
|
const org3Project3 = await findOrCreateProject("batch-limit-project-3", org3, user.id);
|
|
|
|
console.log("tenants.json");
|
|
console.log(
|
|
JSON.stringify({
|
|
apiUrl: "http://localhost:3030",
|
|
tenants: [
|
|
{
|
|
id: org1Project1.project.externalRef,
|
|
secretKey: org1Project1.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org1Project2.project.externalRef,
|
|
secretKey: org1Project2.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org1Project3.project.externalRef,
|
|
secretKey: org1Project3.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org2Project1.project.externalRef,
|
|
secretKey: org2Project1.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org2Project2.project.externalRef,
|
|
secretKey: org2Project2.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org2Project3.project.externalRef,
|
|
secretKey: org2Project3.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org3Project1.project.externalRef,
|
|
secretKey: org3Project1.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org3Project2.project.externalRef,
|
|
secretKey: org3Project2.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
{
|
|
id: org3Project3.project.externalRef,
|
|
secretKey: org3Project3.environments.find((e) => e.type === "DEVELOPMENT")?.apiKey,
|
|
},
|
|
],
|
|
})
|
|
);
|
|
}
|
|
|
|
seed()
|
|
.catch((e) => {
|
|
console.error("❌ Seed failed:");
|
|
console.error(e);
|
|
process.exit(1);
|
|
})
|
|
.finally(async () => {
|
|
await prisma.$disconnect();
|
|
});
|
|
|
|
async function findOrCreateOrganization(
|
|
title: string,
|
|
user: User,
|
|
updates?: Prisma.OrganizationUpdateInput
|
|
) {
|
|
let organization = await prisma.organization.findFirst({
|
|
where: {
|
|
title: title,
|
|
members: {
|
|
some: {
|
|
userId: user.id,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!organization) {
|
|
console.log(`Creating organization: ${title}...`);
|
|
organization = await createOrganization({
|
|
title: title,
|
|
userId: user.id,
|
|
companySize: "1-10",
|
|
});
|
|
}
|
|
|
|
if (updates) {
|
|
organization = await prisma.organization.update({
|
|
where: { id: organization.id },
|
|
data: updates,
|
|
});
|
|
}
|
|
|
|
return organization;
|
|
}
|
|
|
|
async function findOrCreateProject(
|
|
name: string,
|
|
organization: Organization,
|
|
userId: string,
|
|
externalRef?: string
|
|
) {
|
|
let project = await prisma.project.findFirst({
|
|
where: {
|
|
name,
|
|
organizationId: organization.id,
|
|
},
|
|
});
|
|
|
|
if (!project) {
|
|
console.log(`Creating project: ${name}...`);
|
|
project = await createProject({
|
|
organizationSlug: organization.slug,
|
|
name,
|
|
userId,
|
|
version: "v3",
|
|
});
|
|
|
|
if (externalRef) {
|
|
project = await prisma.project.update({
|
|
where: { id: project.id },
|
|
data: { externalRef },
|
|
});
|
|
}
|
|
}
|
|
|
|
console.log(`✅ Project ready: ${project.name} (${project.externalRef})`);
|
|
|
|
// list environments for this project
|
|
const environments = await prisma.runtimeEnvironment.findMany({
|
|
where: { projectId: project.id },
|
|
select: {
|
|
slug: true,
|
|
type: true,
|
|
apiKey: true,
|
|
},
|
|
});
|
|
console.log(` Environments for ${project.name}:`);
|
|
for (const env of environments) {
|
|
console.log(` - ${env.type.toLowerCase()} (${env.slug}): ${env.apiKey}`);
|
|
}
|
|
|
|
return { project, environments };
|
|
}
|
|
|
|
async function ensureDefaultWorkerGroup() {
|
|
// Check if the feature flag already exists
|
|
const existingFlag = await prisma.featureFlag.findUnique({
|
|
where: { key: "defaultWorkerInstanceGroupId" },
|
|
});
|
|
|
|
if (existingFlag) {
|
|
console.log(`✅ Default worker instance group already configured`);
|
|
return;
|
|
}
|
|
|
|
// Check if a managed worker group already exists
|
|
let workerGroup = await prisma.workerInstanceGroup.findFirst({
|
|
where: { type: "MANAGED" },
|
|
});
|
|
|
|
if (!workerGroup) {
|
|
console.log("Creating default worker instance group...");
|
|
|
|
const { createHash, randomBytes } = await import("crypto");
|
|
const tokenValue = `tr_wgt_${randomBytes(20).toString("hex")}`;
|
|
const tokenHash = createHash("sha256").update(tokenValue).digest("hex");
|
|
|
|
const token = await prisma.workerGroupToken.create({
|
|
data: { tokenHash },
|
|
});
|
|
|
|
workerGroup = await prisma.workerInstanceGroup.create({
|
|
data: {
|
|
type: "MANAGED",
|
|
name: "local-dev",
|
|
masterQueue: "local-dev",
|
|
description: "Local development worker group",
|
|
tokenId: token.id,
|
|
},
|
|
});
|
|
|
|
console.log(`✅ Created worker instance group: ${workerGroup.name} (${workerGroup.id})`);
|
|
} else {
|
|
console.log(
|
|
`✅ Worker instance group already exists: ${workerGroup.name} (${workerGroup.id})`
|
|
);
|
|
}
|
|
|
|
// Set the feature flag
|
|
await prisma.featureFlag.upsert({
|
|
where: { key: "defaultWorkerInstanceGroupId" },
|
|
create: {
|
|
key: "defaultWorkerInstanceGroupId",
|
|
value: workerGroup.id,
|
|
},
|
|
update: {
|
|
value: workerGroup.id,
|
|
},
|
|
});
|
|
|
|
console.log(`✅ Set defaultWorkerInstanceGroupId feature flag`);
|
|
}
|