Skip to content

Commit 80b47f2

Browse files
committed
fix(llm-catalog): refresh default model pricing on sync
- Rebuild llm_pricing_tiers and llm_prices in syncLlmCatalog for source=default - Add vitest config, sync regression tests, and pin vitest 3.1.4 - Update pnpm-lock.yaml for the new devDependency
1 parent 8244ac6 commit 80b47f2

File tree

5 files changed

+192
-17
lines changed

5 files changed

+192
-17
lines changed

internal-packages/llm-model-catalog/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@
99
"@trigger.dev/core": "workspace:*",
1010
"@trigger.dev/database": "workspace:*"
1111
},
12+
"devDependencies": {
13+
"vitest": "3.1.4"
14+
},
1215
"scripts": {
16+
"test": "vitest --sequence.concurrent=false --no-file-parallelism",
1317
"typecheck": "tsc --noEmit",
1418
"generate": "node scripts/generate.mjs",
1519
"sync-prices": "bash scripts/sync-model-prices.sh && node scripts/generate.mjs",
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { syncLlmCatalog } from "./sync.js";
3+
import { defaultModelPrices } from "./defaultPrices.js";
4+
5+
const gpt4oDef = defaultModelPrices.find((m) => m.modelName === "gpt-4o");
6+
if (!gpt4oDef) {
7+
throw new Error("expected gpt-4o in defaultModelPrices");
8+
}
9+
10+
describe("syncLlmCatalog", () => {
11+
it("rebuilds pricing tiers and prices for existing default-source models", async () => {
12+
const existingId = "existing-gpt4o";
13+
14+
const llmModelUpdate = vi.fn();
15+
const llmPricingTierDeleteMany = vi.fn();
16+
const llmPricingTierCreate = vi.fn();
17+
18+
const prisma = {
19+
llmModel: {
20+
findFirst: vi.fn(async (args: { where: { modelName: string } }) => {
21+
if (args.where.modelName === "gpt-4o") {
22+
return {
23+
id: existingId,
24+
source: "default",
25+
provider: "openai",
26+
description: "stale description",
27+
contextWindow: 999,
28+
maxOutputTokens: 888,
29+
capabilities: ["legacy"],
30+
isHidden: true,
31+
baseModelName: "legacy-base",
32+
};
33+
}
34+
return null;
35+
}),
36+
},
37+
$transaction: vi.fn(async (fn: (tx: unknown) => Promise<void>) => {
38+
await fn({
39+
llmModel: { update: llmModelUpdate },
40+
llmPricingTier: {
41+
deleteMany: llmPricingTierDeleteMany,
42+
create: llmPricingTierCreate,
43+
},
44+
});
45+
}),
46+
};
47+
48+
await syncLlmCatalog(prisma as never);
49+
50+
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
51+
52+
expect(llmModelUpdate).toHaveBeenCalledWith({
53+
where: { id: existingId },
54+
data: expect.objectContaining({
55+
matchPattern: gpt4oDef.matchPattern,
56+
startDate: gpt4oDef.startDate ? new Date(gpt4oDef.startDate) : null,
57+
}),
58+
});
59+
60+
expect(llmPricingTierDeleteMany).toHaveBeenCalledWith({
61+
where: { modelId: existingId },
62+
});
63+
64+
expect(llmPricingTierCreate).toHaveBeenCalledTimes(gpt4oDef.pricingTiers.length);
65+
66+
const firstTier = gpt4oDef.pricingTiers[0];
67+
expect(llmPricingTierCreate).toHaveBeenCalledWith({
68+
data: {
69+
modelId: existingId,
70+
name: firstTier.name,
71+
isDefault: firstTier.isDefault,
72+
priority: firstTier.priority,
73+
conditions: firstTier.conditions,
74+
prices: {
75+
create: expect.arrayContaining(
76+
Object.entries(firstTier.prices).map(([usageType, price]) => ({
77+
modelId: existingId,
78+
usageType,
79+
price,
80+
}))
81+
),
82+
},
83+
},
84+
});
85+
86+
const createCall = llmPricingTierCreate.mock.calls[0][0] as {
87+
data: { prices: { create: { usageType: string; price: number; modelId: string }[] } };
88+
};
89+
expect(createCall.data.prices.create).toHaveLength(Object.keys(firstTier.prices).length);
90+
});
91+
92+
it("does not rebuild pricing for non-default source models", async () => {
93+
const prisma = {
94+
llmModel: {
95+
findFirst: vi.fn(async (args: { where: { modelName: string } }) => {
96+
if (args.where.modelName === "gpt-4o") {
97+
return {
98+
id: "admin-edited",
99+
source: "admin",
100+
provider: null,
101+
description: null,
102+
contextWindow: null,
103+
maxOutputTokens: null,
104+
capabilities: [],
105+
isHidden: false,
106+
baseModelName: null,
107+
};
108+
}
109+
return null;
110+
}),
111+
},
112+
$transaction: vi.fn(),
113+
};
114+
115+
const result = await syncLlmCatalog(prisma as never);
116+
117+
expect(prisma.$transaction).not.toHaveBeenCalled();
118+
expect(result.modelsUpdated).toBe(0);
119+
expect(result.modelsSkipped).toBeGreaterThan(0);
120+
});
121+
});

internal-packages/llm-model-catalog/src/sync.ts

Lines changed: 47 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,27 @@
1-
import type { PrismaClient } from "@trigger.dev/database";
1+
import type { Prisma, PrismaClient } from "@trigger.dev/database";
22
import { defaultModelPrices } from "./defaultPrices.js";
33
import { modelCatalog } from "./modelCatalog.js";
4+
import type { DefaultModelDefinition } from "./types.js";
5+
6+
function pricingTierCreateData(
7+
modelId: string,
8+
tier: DefaultModelDefinition["pricingTiers"][number]
9+
): Prisma.LlmPricingTierUncheckedCreateInput {
10+
return {
11+
modelId,
12+
name: tier.name,
13+
isDefault: tier.isDefault,
14+
priority: tier.priority,
15+
conditions: tier.conditions,
16+
prices: {
17+
create: Object.entries(tier.prices).map(([usageType, price]) => ({
18+
modelId,
19+
usageType,
20+
price,
21+
})),
22+
},
23+
};
24+
}
425

526
export async function syncLlmCatalog(prisma: PrismaClient): Promise<{
627
modelsUpdated: number;
@@ -31,21 +52,31 @@ export async function syncLlmCatalog(prisma: PrismaClient): Promise<{
3152

3253
const catalog = modelCatalog[modelDef.modelName];
3354

34-
await prisma.llmModel.update({
35-
where: { id: existing.id },
36-
data: {
37-
// Update match pattern and start date from Langfuse (may have changed)
38-
matchPattern: modelDef.matchPattern,
39-
startDate: modelDef.startDate ? new Date(modelDef.startDate) : null,
40-
// Update catalog metadata
41-
provider: catalog?.provider ?? existing.provider,
42-
description: catalog?.description ?? existing.description,
43-
contextWindow: catalog?.contextWindow ?? existing.contextWindow,
44-
maxOutputTokens: catalog?.maxOutputTokens ?? existing.maxOutputTokens,
45-
capabilities: catalog?.capabilities ?? existing.capabilities,
46-
isHidden: catalog?.isHidden ?? existing.isHidden,
47-
baseModelName: catalog?.baseModelName ?? existing.baseModelName,
48-
},
55+
await prisma.$transaction(async (tx) => {
56+
await tx.llmModel.update({
57+
where: { id: existing.id },
58+
data: {
59+
// Update match pattern and start date from Langfuse (may have changed)
60+
matchPattern: modelDef.matchPattern,
61+
startDate: modelDef.startDate ? new Date(modelDef.startDate) : null,
62+
// Update catalog metadata
63+
provider: catalog?.provider ?? existing.provider,
64+
description: catalog?.description ?? existing.description,
65+
contextWindow: catalog?.contextWindow ?? existing.contextWindow,
66+
maxOutputTokens: catalog?.maxOutputTokens ?? existing.maxOutputTokens,
67+
capabilities: catalog?.capabilities ?? existing.capabilities,
68+
isHidden: catalog?.isHidden ?? existing.isHidden,
69+
baseModelName: catalog?.baseModelName ?? existing.baseModelName,
70+
},
71+
});
72+
73+
await tx.llmPricingTier.deleteMany({ where: { modelId: existing.id } });
74+
75+
for (const tier of modelDef.pricingTiers) {
76+
await tx.llmPricingTier.create({
77+
data: pricingTierCreateData(existing.id, tier),
78+
});
79+
}
4980
});
5081

5182
modelsUpdated++;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { defineConfig } from "vitest/config";
2+
3+
export default defineConfig({
4+
test: {
5+
include: ["**/*.test.ts"],
6+
globals: true,
7+
isolate: true,
8+
fileParallelism: false,
9+
poolOptions: {
10+
threads: {
11+
singleThread: true,
12+
},
13+
},
14+
},
15+
});

pnpm-lock.yaml

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)