Skip to content

Commit ab71e3b

Browse files
authored
Merge branch 'main' into renovate/major-major-upgrades-(manual)
2 parents fc82295 + 59f0f9d commit ab71e3b

File tree

19 files changed

+861
-78
lines changed

19 files changed

+861
-78
lines changed

apps/web/src/app/api/ai/journl-agent/route.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { after } from "next/server";
12
import { journlAgent, setJournlRuntimeContext } from "~/ai/agents/journl-agent";
23
import type { JournlAgentContext } from "~/ai/agents/journl-agent-context";
34
import { handler as corsHandler } from "~/app/api/_cors/cors";
@@ -18,30 +19,6 @@ async function handler(req: Request) {
1819

1920
const result = await journlAgent.streamVNext(messages, {
2021
format: "aisdk",
21-
onFinish: async (result) => {
22-
const modelData = await journlAgent.getModel();
23-
24-
const provider = modelData.provider;
25-
const model = modelData.modelId;
26-
27-
if (result.usage && session.user?.id) {
28-
await api.usage.trackModelUsage({
29-
metrics: [
30-
{
31-
quantity: result.usage.promptTokens,
32-
unit: "input_tokens",
33-
},
34-
{
35-
quantity: result.usage.completionTokens,
36-
unit: "output_tokens",
37-
},
38-
],
39-
model_id: model,
40-
model_provider: provider,
41-
user_id: session.user.id,
42-
});
43-
}
44-
},
4522
runtimeContext: setJournlRuntimeContext({
4623
...rest.context,
4724
user: {
@@ -51,6 +28,43 @@ async function handler(req: Request) {
5128
} satisfies JournlAgentContext),
5229
});
5330

31+
if (session.user?.id) {
32+
after(async () => {
33+
try {
34+
const fullOutput = await result.getFullOutput();
35+
const usage = fullOutput.usage;
36+
37+
if (usage) {
38+
const modelData = await journlAgent.getModel();
39+
const provider = modelData.provider;
40+
const model = modelData.modelId;
41+
42+
await api.usage.trackModelUsage({
43+
metrics: [
44+
{
45+
quantity: usage.promptTokens || 0,
46+
unit: "input_tokens",
47+
},
48+
{
49+
quantity: usage.completionTokens || 0,
50+
unit: "output_tokens",
51+
},
52+
{
53+
quantity: usage.reasoningTokens || 0,
54+
unit: "reasoning_tokens",
55+
},
56+
],
57+
model_id: model,
58+
model_provider: provider,
59+
user_id: session.user.id,
60+
});
61+
}
62+
} catch (error) {
63+
console.error("[usage tracking] error:", error);
64+
}
65+
});
66+
}
67+
5468
return result.toUIMessageStreamResponse();
5569
} catch (error) {
5670
console.error("[api.chat.route] error 👀", error);
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { zUsageEventWebhook } from "@acme/db/schema";
2+
import { NextResponse } from "next/server";
3+
import { api } from "~/trpc/server";
4+
import { handler } from "../_lib/webhook-handler";
5+
6+
/**
7+
* This webhook processes usage events when they are created or updated.
8+
* Usage events are created when AI features are used (chat, embedding, etc.)
9+
*/
10+
export const POST = handler(zUsageEventWebhook, async (payload) => {
11+
// Skip DELETE events
12+
if (payload.type === "DELETE") {
13+
return NextResponse.json({ success: true });
14+
}
15+
16+
// Skip processing if the event is already processed
17+
if (payload.record?.status === "processed") {
18+
return NextResponse.json({ success: true });
19+
}
20+
21+
try {
22+
// Process the usage event with the usage period
23+
const result = await api.usage.processUsageEvent({
24+
usage_event_id: payload.record.id,
25+
user_id: payload.record.user_id,
26+
});
27+
28+
return NextResponse.json({ result, success: true });
29+
} catch (error) {
30+
return NextResponse.json(
31+
{
32+
details: error instanceof Error ? error.message : "Unknown error",
33+
error: "Processing failed",
34+
success: false,
35+
},
36+
{ status: 500 },
37+
);
38+
}
39+
});

packages/api/src/api-router/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createTRPCRouter } from "../trpc.js";
22
import { authRouter } from "./auth.js";
33
import { documentRouter } from "./document.js";
44
import { journalRouter } from "./journal.js";
5+
import { modelPricingRouter } from "./model-pricing.js";
56
import { notesRouter } from "./notes.js";
67
import { pagesRouter } from "./pages.js";
78
import { subscriptionRouter } from "./subscription.js";
@@ -11,6 +12,7 @@ export const apiRouter = createTRPCRouter({
1112
auth: authRouter,
1213
document: documentRouter,
1314
journal: journalRouter,
15+
modelPricing: modelPricingRouter,
1416
notes: notesRouter,
1517
pages: pagesRouter,
1618
subscription: subscriptionRouter,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { and, desc, eq, lte } from "@acme/db";
2+
import { ModelPricing, zInsertModelPricing } from "@acme/db/schema";
3+
import { TRPCError, type TRPCRouterRecord } from "@trpc/server";
4+
import { z } from "zod/v4";
5+
import { publicProcedure } from "../trpc.js";
6+
7+
export const modelPricingRouter = {
8+
getAllPricingForModel: publicProcedure
9+
.input(
10+
z.object({
11+
model_id: z.string(),
12+
model_provider: z.string(),
13+
}),
14+
)
15+
.query(async ({ ctx, input }) => {
16+
try {
17+
const pricing = await ctx.db.query.ModelPricing.findMany({
18+
orderBy: [desc(ModelPricing.effective_date)],
19+
where: and(
20+
eq(ModelPricing.model_id, input.model_id),
21+
eq(ModelPricing.model_provider, input.model_provider),
22+
lte(ModelPricing.effective_date, new Date().toISOString()),
23+
),
24+
});
25+
26+
return pricing;
27+
} catch (error) {
28+
console.error(
29+
"Database error in modelPricing.getAllPricingForModel:",
30+
error,
31+
);
32+
throw new TRPCError({
33+
code: "INTERNAL_SERVER_ERROR",
34+
message: "Failed to get model pricing",
35+
});
36+
}
37+
}),
38+
getCurrentPricing: publicProcedure
39+
.input(
40+
z.object({
41+
model_id: z.string(),
42+
model_provider: z.string(),
43+
unit_type: z.string(),
44+
}),
45+
)
46+
.query(async ({ ctx, input }) => {
47+
try {
48+
const pricing = await ctx.db.query.ModelPricing.findFirst({
49+
orderBy: [desc(ModelPricing.effective_date)],
50+
where: and(
51+
eq(ModelPricing.model_id, input.model_id),
52+
eq(ModelPricing.model_provider, input.model_provider),
53+
eq(ModelPricing.unit_type, input.unit_type),
54+
lte(ModelPricing.effective_date, new Date().toISOString()),
55+
),
56+
});
57+
58+
return pricing;
59+
} catch (error) {
60+
console.error(
61+
"Database error in modelPricing.getCurrentPricing:",
62+
error,
63+
);
64+
throw new TRPCError({
65+
code: "INTERNAL_SERVER_ERROR",
66+
message: "Failed to get current pricing",
67+
});
68+
}
69+
}),
70+
71+
upsertPricing: publicProcedure
72+
.input(zInsertModelPricing)
73+
.mutation(async ({ ctx, input }) => {
74+
try {
75+
const [pricing] = await ctx.db
76+
.insert(ModelPricing)
77+
.values(input)
78+
.onConflictDoUpdate({
79+
set: {
80+
price_per_unit: input.price_per_unit,
81+
},
82+
target: [
83+
ModelPricing.model_id,
84+
ModelPricing.model_provider,
85+
ModelPricing.unit_type,
86+
ModelPricing.effective_date,
87+
],
88+
})
89+
.returning();
90+
91+
return pricing;
92+
} catch (error) {
93+
console.error("Database error in modelPricing.upsertPricing:", error);
94+
throw new TRPCError({
95+
code: "INTERNAL_SERVER_ERROR",
96+
message: "Failed to upsert pricing",
97+
});
98+
}
99+
}),
100+
} satisfies TRPCRouterRecord;

packages/api/src/api-router/subscription.ts

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { and, eq, or } from "@acme/db";
2-
import { Subscription } from "@acme/db/schema";
31
import type { TRPCRouterRecord } from "@trpc/server";
42
import { z } from "zod/v4";
3+
import { getActiveSubscription } from "../shared/subscription";
54
import { protectedProcedure } from "../trpc";
65

76
/**
@@ -66,36 +65,8 @@ export const subscriptionRouter = {
6665
quota: plan.quota,
6766
});
6867
}),
69-
// getActivePlan: protectedProcedure.query(async ({ ctx }) => {
70-
// const activeSubscription = await getActiveSubscription({ ctx });
71-
// if (!activeSubscription?.priceId) return null;
72-
73-
// const plan = await ctx.db.query.Plan.findFirst({
74-
// where: eq(Plan.id, activeSubscription.priceId),
75-
// with: {
76-
// prices: true,
77-
// },
78-
// });
79-
80-
// return plan;
81-
// }),
8268
getSubscription: protectedProcedure.query(async ({ ctx }) => {
83-
const subscription = await ctx.db.query.Subscription.findFirst({
84-
where: and(
85-
eq(Subscription.referenceId, ctx.session.user.id),
86-
or(
87-
eq(Subscription.status, "active"),
88-
eq(Subscription.status, "trialing"),
89-
),
90-
),
91-
with: {
92-
plan: {
93-
with: {
94-
price: true,
95-
},
96-
},
97-
},
98-
});
69+
const subscription = await getActiveSubscription({ ctx });
9970

10071
if (!subscription?.plan) {
10172
return null;

0 commit comments

Comments
 (0)