Skip to content

Commit e889c1d

Browse files
Dshamirclaude
andcommitted
feat: credit enforcement on chat, workflow export/import
- Wire up monthly credit checking and incrementing for both chat streaming routes. Credits checked before LLM call (429 if exceeded), incremented after successful response. Auto-resets when past reset date. Limit via MONTHLY_CREDIT_LIMIT env var (PR Open-Legal-Products#157). - Add GET /workflows/:id/export (.mikeworkflow.json download) and POST /workflows/import endpoints for portable workflow transfer between environments (PR Open-Legal-Products#59). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent eb70e06 commit e889c1d

4 files changed

Lines changed: 390 additions & 241 deletions

File tree

backend/src/lib/credits.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { prisma } from "./prisma";
2+
import { logger } from "./logger";
3+
4+
const MONTHLY_CREDIT_LIMIT = Number(process.env.MONTHLY_CREDIT_LIMIT) || 999999;
5+
6+
export async function checkCredits(
7+
userId: string,
8+
): Promise<{ ok: true } | { ok: false; detail: string }> {
9+
const profile = await prisma.userProfile.findUnique({
10+
where: { userId },
11+
select: { messageCreditsUsed: true, creditsResetDate: true },
12+
});
13+
if (!profile) return { ok: true };
14+
15+
if (profile.creditsResetDate && new Date() > new Date(profile.creditsResetDate)) {
16+
const creditsResetDate = new Date();
17+
creditsResetDate.setDate(creditsResetDate.getDate() + 30);
18+
await prisma.userProfile.update({
19+
where: { userId },
20+
data: { messageCreditsUsed: 0, creditsResetDate },
21+
});
22+
return { ok: true };
23+
}
24+
25+
if ((profile.messageCreditsUsed ?? 0) >= MONTHLY_CREDIT_LIMIT) {
26+
return {
27+
ok: false,
28+
detail: "Monthly message credit limit reached. Please upgrade or wait for reset.",
29+
};
30+
}
31+
return { ok: true };
32+
}
33+
34+
export async function incrementCredits(userId: string): Promise<void> {
35+
try {
36+
await prisma.userProfile.updateMany({
37+
where: { userId },
38+
data: { messageCreditsUsed: { increment: 1 } },
39+
});
40+
} catch (err) {
41+
logger.warn({ err, userId }, "[credits] failed to increment credits");
42+
}
43+
}

backend/src/routes/chat.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { checkProjectAccess } from "../lib/access";
1616
import { logger } from "../lib/logger";
1717
import { auditLog } from "../lib/audit";
1818
import { withStreamTimeout, StreamTimeoutError } from "../lib/streamTimeout";
19+
import { checkCredits, incrementCredits } from "../lib/credits";
1920

2021
export const chatRouter = Router();
2122

@@ -473,6 +474,11 @@ chatRouter.post("/", requireAuth, async (req, res) => {
473474

474475
const workflowStore = await buildWorkflowStore(userId, userEmail);
475476

477+
const creditCheck = await checkCredits(userId);
478+
if (!creditCheck.ok) {
479+
return void res.status(429).json({ detail: creditCheck.detail });
480+
}
481+
476482
devLog("[chat/stream] starting LLM stream", {
477483
apiMessageCount: apiMessages.length,
478484
docCount: Object.keys(docIndex).length,
@@ -527,6 +533,8 @@ chatRouter.post("/", requireAuth, async (req, res) => {
527533
data: { title: lastUser.content.slice(0, 120) },
528534
});
529535
}
536+
537+
await incrementCredits(userId);
530538
} catch (err) {
531539
if (err instanceof StreamTimeoutError) {
532540
logger.warn({ chatId }, "[chat/stream] LLM stream timed out");

backend/src/routes/projectChat.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { logger } from "../lib/logger";
1818
import { withStreamTimeout, StreamTimeoutError } from "../lib/streamTimeout";
1919
import { validate } from "../lib/validation";
2020
import { zodProjectChatBody } from "../lib/validation/common";
21+
import { checkCredits, incrementCredits } from "../lib/credits";
2122

2223
const PROJECT_SYSTEM_PROMPT_EXTRA = `PROJECT CONTEXT:
2324
You are operating within a project folder that contains a collection of legal documents the user has organised for a single matter. The user's questions will usually refer to one or more documents in this project — your job is to find the relevant files to work on. Use list_documents to see what is available and fetch_documents / read_document to pull in any documents you need before answering.
@@ -122,6 +123,11 @@ projectChatRouter.post("/", validate(zodProjectChatBody), requireAuth, async (re
122123

123124
const workflowStore = await buildWorkflowStore(userId, userEmail);
124125

126+
const creditCheck = await checkCredits(userId);
127+
if (!creditCheck.ok) {
128+
return void res.status(429).json({ detail: creditCheck.detail });
129+
}
130+
125131
res.setHeader("Content-Type", "text/event-stream");
126132
res.setHeader("Cache-Control", "no-cache");
127133
res.setHeader("Connection", "keep-alive");
@@ -166,6 +172,8 @@ projectChatRouter.post("/", validate(zodProjectChatBody), requireAuth, async (re
166172
data: { title: lastUser.content.slice(0, 120) },
167173
});
168174
}
175+
176+
await incrementCredits(userId);
169177
} catch (err) {
170178
if (err instanceof StreamTimeoutError) {
171179
logger.warn({ chatId }, "[project-chat/stream] LLM stream timed out");

0 commit comments

Comments
 (0)