fix: return error in mcp and activity when there are no credits

This commit is contained in:
Harshith Mullapudi 2025-10-18 00:18:35 +05:30
parent 6732ff71c5
commit d0126797de
7 changed files with 141 additions and 102 deletions

View File

@ -3,6 +3,7 @@ import { IngestionStatus } from "@core/database";
import { EpisodeType } from "@core/types";
import { type z } from "zod";
import { prisma } from "~/db.server";
import { hasCredits } from "~/services/billing.server";
import { type IngestBodyRequest, ingestTask } from "~/trigger/ingest/ingest";
import { ingestDocumentTask } from "~/trigger/ingest/ingest-document";
@ -27,6 +28,16 @@ export const addToQueue = async (
);
}
// Check if workspace has sufficient credits before processing
const hasSufficientCredits = await hasCredits(
user.Workspace?.id as string,
"addEpisode",
);
if (!hasSufficientCredits) {
throw new Error("no credits");
}
const queuePersist = await prisma.ingestionQueue.create({
data: {
data: body,

View File

@ -75,11 +75,13 @@ const { action, loader } = createActionApiRoute(
if (user.Workspace?.id) {
try {
await triggerWebhookDelivery(activity.id, user.Workspace.id);
logger.log("Webhook delivery triggered for activity", { activityId: activity.id });
logger.log("Webhook delivery triggered for activity", {
activityId: activity.id,
});
} catch (webhookError) {
logger.error("Failed to trigger webhook delivery", {
activityId: activity.id,
error: webhookError
logger.error("Failed to trigger webhook delivery", {
activityId: activity.id,
error: webhookError,
});
// Don't fail the entire request if webhook delivery fails
}

View File

@ -6,7 +6,11 @@
*/
import { prisma } from "~/db.server";
import { getPlanConfig } from "~/config/billing.server";
import {
BILLING_CONFIG,
getPlanConfig,
isBillingEnabled,
} from "~/config/billing.server";
import type { PlanType, Subscription } from "@prisma/client";
export type CreditOperation = "addEpisode" | "search" | "chatMessage";
@ -221,3 +225,51 @@ export async function getUsageSummary(workspaceId: string) {
},
};
}
/**
* Check if workspace has sufficient credits
*/
export async function hasCredits(
workspaceId: string,
operation: CreditOperation,
amount?: number,
): Promise<boolean> {
// If billing is disabled, always return true
if (!isBillingEnabled()) {
return true;
}
const creditCost = amount || BILLING_CONFIG.creditCosts[operation];
const workspace = await prisma.workspace.findUnique({
where: { id: workspaceId },
include: {
Subscription: true,
user: {
include: {
UserUsage: true,
},
},
},
});
if (!workspace?.user?.UserUsage || !workspace.Subscription) {
return false;
}
const userUsage = workspace.user.UserUsage;
// const subscription = workspace.Subscription;
// If has available credits, return true
if (userUsage.availableCredits >= creditCost) {
return true;
}
// If overage is enabled (Pro/Max), return true
// if (subscription.enableUsageBilling) {
// return true;
// }
// Free plan with no credits left
return false;
}

View File

@ -650,6 +650,23 @@ export async function deductCredits(
}),
]);
} else {
await prisma.userUsage.update({
where: { id: userUsage.id },
data: {
availableCredits: 0,
usedCredits: userUsage.usedCredits + creditCost,
// Update usage breakdown
...(operation === "addEpisode" && {
episodeCreditsUsed: userUsage.episodeCreditsUsed + creditCost,
}),
...(operation === "search" && {
searchCreditsUsed: userUsage.searchCreditsUsed + creditCost,
}),
...(operation === "chatMessage" && {
chatCreditsUsed: userUsage.chatCreditsUsed + creditCost,
}),
},
});
}
}
}

View File

@ -5,6 +5,8 @@ import { SearchService } from "~/services/search.server";
import { SpaceService } from "~/services/space.server";
import { deepSearch } from "~/trigger/deep-search";
import { IntegrationLoader } from "./integration-loader";
import { hasCredits } from "~/services/billing.server";
import { prisma } from "~/db.server";
const searchService = new SearchService();
const spaceService = new SpaceService();
@ -277,6 +279,30 @@ async function handleUserProfile(userId: string) {
// Handler for memory_ingest
async function handleMemoryIngest(args: any) {
try {
const workspace = await prisma.workspace.findFirst({
where: {
userId: args.userId,
},
});
// Check if workspace has sufficient credits before processing
const hasSufficientCredits = await hasCredits(
workspace?.id as string,
"addEpisode",
);
if (!hasSufficientCredits) {
return {
content: [
{
type: "text",
text: `Error ingesting data: your credits have expired`,
},
],
isError: true,
};
}
// Use spaceIds from args if provided, otherwise use spaceId from query params
const spaceIds =
args.spaceIds || (args.spaceId ? [args.spaceId] : undefined);

View File

@ -79,8 +79,8 @@
"@tiptap/pm": "^2.11.9",
"@tiptap/react": "^2.11.9",
"@tiptap/starter-kit": "2.11.9",
"@trigger.dev/react-hooks": "4.0.0-v4-beta.22",
"@trigger.dev/sdk": "4.0.0-v4-beta.22",
"@trigger.dev/react-hooks": "4.0.4",
"@trigger.dev/sdk": "4.0.4",
"ai": "4.3.19",
"axios": "^1.10.0",
"bullmq": "^5.53.2",
@ -152,7 +152,7 @@
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@tailwindcss/vite": "^4.1.7",
"@trigger.dev/build": "4.0.0-v4-beta.22",
"@trigger.dev/build": "4.0.4",
"@types/compression": "^1.7.2",
"@types/d3": "^7.4.3",
"@types/express": "^4.17.13",

119
pnpm-lock.yaml generated
View File

@ -476,11 +476,11 @@ importers:
specifier: 2.11.9
version: 2.11.9
'@trigger.dev/react-hooks':
specifier: 4.0.0-v4-beta.22
version: 4.0.0-v4-beta.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
specifier: 4.0.4
version: 4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@trigger.dev/sdk':
specifier: 4.0.0-v4-beta.22
version: 4.0.0-v4-beta.22(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)
specifier: 4.0.4
version: 4.0.4(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)
ai:
specifier: 4.3.19
version: 4.3.19(react@18.3.1)(zod@3.25.76)
@ -690,8 +690,8 @@ importers:
specifier: ^4.1.7
version: 4.1.9(vite@6.3.5(@types/node@20.19.7)(jiti@2.4.2)(less@4.4.0)(lightningcss@1.30.1)(sass@1.89.2)(terser@5.42.0)(tsx@4.20.4)(yaml@2.8.0))
'@trigger.dev/build':
specifier: 4.0.0-v4-beta.22
version: 4.0.0-v4-beta.22(typescript@5.8.3)
specifier: 4.0.4
version: 4.0.4(typescript@5.8.3)
'@types/compression':
specifier: ^1.7.2
version: 1.8.1
@ -2863,12 +2863,6 @@ packages:
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/core@1.30.1':
resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/core@2.0.1':
resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==}
engines: {node: ^18.19.0 || >=20.6.0}
@ -3053,10 +3047,6 @@ packages:
resolution: {integrity: sha512-ZDjMJJQRlyk8A1KZFCc+bCbsyrn1wTwdNt56F7twdfUfnHUZUq77/WfONCj8p72NZOyP7pNTdUWSTYC3GTbuuQ==}
engines: {node: '>=14'}
'@opentelemetry/semantic-conventions@1.28.0':
resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==}
engines: {node: '>=14'}
'@opentelemetry/semantic-conventions@1.36.0':
resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==}
engines: {node: '>=14'}
@ -5538,31 +5528,27 @@ packages:
'@tiptap/core': ^2.7.0
'@tiptap/pm': ^2.7.0
'@trigger.dev/build@4.0.0-v4-beta.22':
resolution: {integrity: sha512-W+SfA7soXzD7f2GQQL2Q4x3+JQkJijcVjgKpNuwPdZcgI++YpJkPzJ5RlT98flErU8ZvuiL26SAur2tvObrZgA==}
'@trigger.dev/build@4.0.4':
resolution: {integrity: sha512-W3mP+RBkcYOrNYTTmQ/WdU6LB+2Tk1S6r3OjEWqXEPsXLEEw6BzHTHZBirHYX4lWRBL9jVkL+/H74ycyNfzRjg==}
engines: {node: '>=18.20.0'}
'@trigger.dev/core@4.0.0-v4-beta.22':
resolution: {integrity: sha512-FVaVNsW3KQgYEWStr80Iu+1l4KMyHPVU4QbV55pLQp7d126jOuP+hXYp7LhnYVZtgcQLIZSC0VjJc/UYwr4D6g==}
'@trigger.dev/core@4.0.4':
resolution: {integrity: sha512-c5myttkNhqaqvLlEz3ttE1qEsULlD6ILBge5FAfEtMv9HVS/pNlgvMKrdFMefaGO/bE4HoxrNGdJsY683Kq32w==}
engines: {node: '>=18.20.0'}
'@trigger.dev/core@4.0.0-v4-beta.27':
resolution: {integrity: sha512-PJzW07GbxeHKigZ0AiO4aAtDdb2r5iioI7P6TLTqp3XfsxLb1ezPNv3zt6dy1uvZsefGR/EO4y7X0VN1pJyLTA==}
engines: {node: '>=18.20.0'}
'@trigger.dev/react-hooks@4.0.0-v4-beta.22':
resolution: {integrity: sha512-hWBoxEkNSM+IcFsUlFEJBcMZGmpaYGPy5k/o+iK9QNLURiQsKEYGYoBzKlA7iP0cVPwhIV1eNlsPediNRQyTsA==}
'@trigger.dev/react-hooks@4.0.4':
resolution: {integrity: sha512-tgyaGKwFTbVaD4QZdR5GBc2R7T/yq+vHpWw506ys75Mo9uEZN0rGmw7g5q1Pe4XJvsdDiVjcxcJ4tK8zwUM5Zg==}
engines: {node: '>=18.20.0'}
peerDependencies:
react: ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^18.0 || ^19.0 || ^19.0.0-rc
'@trigger.dev/sdk@4.0.0-v4-beta.22':
resolution: {integrity: sha512-yRv9G/KODpItU16Iv6gCfLQ2SjhGq443zTlYKY3XZf4HHIuByOhkOKYPRpl82FmJprL8DxnT7V0pma4kfHBzPQ==}
'@trigger.dev/sdk@4.0.4':
resolution: {integrity: sha512-54krRw9SN1CGm5u17JBzu0hNzRf1u37jKbSFFngPJjUOltOgi/owey5+KNu1rGthabhOBK2VKzvKEd4sn08RCA==}
engines: {node: '>=18.20.0'}
peerDependencies:
ai: ^4.2.0
zod: ^3.0.0
ai: ^4.2.0 || ^5.0.0
zod: ^3.0.0 || ^4.0.0
peerDependenciesMeta:
ai:
optional: true
@ -8990,10 +8976,6 @@ packages:
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.get@4.4.2:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
@ -15270,11 +15252,6 @@ snapshots:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.25.1
'@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.28.0
'@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
@ -15524,8 +15501,6 @@ snapshots:
'@opentelemetry/semantic-conventions@1.25.1': {}
'@opentelemetry/semantic-conventions@1.28.0': {}
'@opentelemetry/semantic-conventions@1.36.0': {}
'@oslojs/asn1@1.0.0':
@ -18414,9 +18389,9 @@ snapshots:
'@tiptap/core': 2.25.0(@tiptap/pm@2.25.0)
'@tiptap/pm': 2.25.0
'@trigger.dev/build@4.0.0-v4-beta.22(typescript@5.8.3)':
'@trigger.dev/build@4.0.4(typescript@5.8.3)':
dependencies:
'@trigger.dev/core': 4.0.0-v4-beta.22
'@trigger.dev/core': 4.0.4
pkg-types: 1.3.1
tinyglobby: 0.2.14
tsconfck: 3.1.3(typescript@5.8.3)
@ -18426,46 +18401,7 @@ snapshots:
- typescript
- utf-8-validate
'@trigger.dev/core@4.0.0-v4-beta.22':
dependencies:
'@bugsnag/cuid': 3.2.1
'@electric-sql/client': 1.0.0-beta.1
'@google-cloud/precise-date': 4.0.0
'@jsonhero/path': 1.0.21
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.52.1
'@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-logs-otlp-http': 0.52.1(@opentelemetry/api@1.9.0)
'@opentelemetry/exporter-trace-otlp-http': 0.52.1(@opentelemetry/api@1.9.0)
'@opentelemetry/instrumentation': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0)
'@opentelemetry/resources': 1.25.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-logs': 0.52.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-node': 0.52.1(@opentelemetry/api@1.9.0)(supports-color@10.0.0)
'@opentelemetry/sdk-trace-base': 1.25.1(@opentelemetry/api@1.9.0)
'@opentelemetry/sdk-trace-node': 1.25.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.25.1
dequal: 2.0.3
eventsource: 3.0.7
eventsource-parser: 3.0.3
execa: 8.0.1
humanize-duration: 3.33.0
jose: 5.10.0
nanoid: 3.3.8
prom-client: 15.1.3
socket.io: 4.7.4
socket.io-client: 4.7.5
std-env: 3.9.0
superjson: 2.2.2
tinyexec: 0.3.2
zod: 3.23.8
zod-error: 1.5.0
zod-validation-error: 1.5.0(zod@3.23.8)
transitivePeerDependencies:
- bufferutil
- supports-color
- utf-8-validate
'@trigger.dev/core@4.0.0-v4-beta.27':
'@trigger.dev/core@4.0.4':
dependencies:
'@bugsnag/cuid': 3.2.1
'@electric-sql/client': 1.0.0-beta.1
@ -18488,7 +18424,6 @@ snapshots:
execa: 8.0.1
humanize-duration: 3.33.0
jose: 5.10.0
lodash.get: 4.4.2
nanoid: 3.3.8
prom-client: 15.1.3
socket.io: 4.7.4
@ -18505,9 +18440,9 @@ snapshots:
- supports-color
- utf-8-validate
'@trigger.dev/react-hooks@4.0.0-v4-beta.22(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
'@trigger.dev/react-hooks@4.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@trigger.dev/core': 4.0.0-v4-beta.27
'@trigger.dev/core': 4.0.4
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
swr: 2.3.3(react@18.3.1)
@ -18516,15 +18451,14 @@ snapshots:
- supports-color
- utf-8-validate
'@trigger.dev/sdk@4.0.0-v4-beta.22(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)':
'@trigger.dev/sdk@4.0.4(ai@4.3.19(react@18.3.1)(zod@3.25.76))(zod@3.25.76)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/api-logs': 0.52.1
'@opentelemetry/semantic-conventions': 1.25.1
'@trigger.dev/core': 4.0.0-v4-beta.22
'@opentelemetry/semantic-conventions': 1.36.0
'@trigger.dev/core': 4.0.4
chalk: 5.4.1
cronstrue: 2.59.0
debug: 4.4.1(supports-color@10.0.0)
debug: 4.4.3
evt: 2.5.9
slug: 6.1.0
ulid: 2.4.0
@ -20393,7 +20327,6 @@ snapshots:
debug@4.4.3:
dependencies:
ms: 2.1.3
optional: true
decamelize-keys@1.1.1:
dependencies:
@ -22568,8 +22501,6 @@ snapshots:
lodash.defaults@4.2.0: {}
lodash.get@4.4.2: {}
lodash.isarguments@3.1.0: {}
lodash.isplainobject@4.0.6: {}