feat: implement error classification and handling for AI generation limits
- Added error classification for daily generation cap and concurrency limits in the PromptNode component, improving user feedback during AI image generation failures. - Enhanced toast notifications to provide specific messages for daily limit and concurrent job errors. - Introduced internal mutations in the credits module to check abuse limits and track usage, ensuring better resource management and user experience. - Updated AI error handling logic to categorize and respond to different error types effectively.
This commit is contained in:
27
convex/ai.ts
27
convex/ai.ts
@@ -1,6 +1,6 @@
|
||||
import { v } from "convex/values";
|
||||
import { action } from "./_generated/server";
|
||||
import { api } from "./_generated/api";
|
||||
import { api, internal } from "./_generated/api";
|
||||
import {
|
||||
generateImageViaOpenRouter,
|
||||
DEFAULT_IMAGE_MODEL,
|
||||
@@ -182,6 +182,9 @@ export const generateImage = action({
|
||||
throw new Error(`Unknown model: ${modelId}`);
|
||||
}
|
||||
|
||||
// Abuse-Check vor allem anderen — immer, unabhängig von Credits
|
||||
await ctx.runMutation(internal.credits.checkAbuseLimits, {});
|
||||
|
||||
const reservationId = internalCreditsEnabled
|
||||
? await ctx.runMutation(api.credits.reserve, {
|
||||
estimatedCost: modelConfig.creditCost,
|
||||
@@ -192,15 +195,21 @@ export const generateImage = action({
|
||||
})
|
||||
: null;
|
||||
|
||||
await ctx.runMutation(api.nodes.updateStatus, {
|
||||
nodeId: args.nodeId,
|
||||
status: "executing",
|
||||
retryCount: 0,
|
||||
});
|
||||
// Usage-Tracking wenn Credits deaktiviert (reserve übernimmt das bei aktivierten Credits)
|
||||
if (!internalCreditsEnabled) {
|
||||
await ctx.runMutation(internal.credits.incrementUsage, {});
|
||||
}
|
||||
|
||||
let retryCount = 0;
|
||||
|
||||
try {
|
||||
// Status auf "executing" setzen — im try-Block damit Fehler den catch erreichen
|
||||
await ctx.runMutation(api.nodes.updateStatus, {
|
||||
nodeId: args.nodeId,
|
||||
status: "executing",
|
||||
retryCount: 0,
|
||||
});
|
||||
|
||||
let referenceImageUrl = args.referenceImageUrl?.trim() || undefined;
|
||||
if (args.referenceStorageId) {
|
||||
referenceImageUrl =
|
||||
@@ -290,6 +299,12 @@ export const generateImage = action({
|
||||
});
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
// Concurrency freigeben wenn Credits deaktiviert
|
||||
// (commit/release übernehmen das bei aktivierten Credits)
|
||||
if (!internalCreditsEnabled) {
|
||||
await ctx.runMutation(internal.credits.decrementConcurrency, {});
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { query, mutation, internalMutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
import { requireAuth } from "./helpers";
|
||||
import { internal } from "./_generated/api";
|
||||
|
||||
// ============================================================================
|
||||
// Tier-Konfiguration
|
||||
@@ -344,14 +345,14 @@ export const reserve = mutation({
|
||||
|
||||
if (dailyUsage && dailyUsage.generationCount >= config.dailyGenerationCap) {
|
||||
throw new Error(
|
||||
`Daily generation limit reached (${config.dailyGenerationCap}/${tier})`
|
||||
`daily_cap:Tageslimit erreicht (${config.dailyGenerationCap} Generierungen/Tag im ${tier}-Tier)`
|
||||
);
|
||||
}
|
||||
|
||||
// Concurrency Limit prüfen
|
||||
if (dailyUsage && dailyUsage.concurrentJobs >= config.concurrencyLimit) {
|
||||
throw new Error(
|
||||
`Concurrent job limit reached (${config.concurrencyLimit}/${tier})`
|
||||
`concurrency:Bereits ${config.concurrencyLimit} Generierung(en) aktiv — bitte warten`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -652,3 +653,109 @@ export const topUp = mutation({
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Internal Mutations — Abuse Prevention (für ai.ts Action)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Prüft Daily Cap und Concurrency — wirft Fehler bei Verstoß.
|
||||
* Wird von generateImage aufgerufen BEVOR Credits reserviert werden.
|
||||
* Wird benötigt, wenn INTERNAL_CREDITS_ENABLED !== "true".
|
||||
*/
|
||||
export const checkAbuseLimits = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const user = await requireAuth(ctx);
|
||||
|
||||
const subscription = await ctx.db
|
||||
.query("subscriptions")
|
||||
.withIndex("by_user", (q) => q.eq("userId", user.userId))
|
||||
.order("desc")
|
||||
.first();
|
||||
const tier = (subscription?.tier ?? "free") as Tier;
|
||||
const config = TIER_CONFIG[tier];
|
||||
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
const usage = await ctx.db
|
||||
.query("dailyUsage")
|
||||
.withIndex("by_user_date", (q) =>
|
||||
q.eq("userId", user.userId).eq("date", today)
|
||||
)
|
||||
.unique();
|
||||
|
||||
const dailyCount = usage?.generationCount ?? 0;
|
||||
if (dailyCount >= config.dailyGenerationCap) {
|
||||
throw new Error(
|
||||
`daily_cap:Tageslimit erreicht (${config.dailyGenerationCap} Generierungen/Tag im ${tier}-Tier)`
|
||||
);
|
||||
}
|
||||
|
||||
const currentConcurrency = usage?.concurrentJobs ?? 0;
|
||||
if (currentConcurrency >= config.concurrencyLimit) {
|
||||
throw new Error(
|
||||
`concurrency:Bereits ${config.concurrencyLimit} Generierung(en) aktiv — bitte warten`
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Erhöht generationCount und concurrentJobs atomar.
|
||||
* Nur aufrufen wenn INTERNAL_CREDITS_ENABLED !== "true"
|
||||
* (reserve übernimmt das bei aktivierten Credits).
|
||||
*/
|
||||
export const incrementUsage = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
const usage = await ctx.db
|
||||
.query("dailyUsage")
|
||||
.withIndex("by_user_date", (q) =>
|
||||
q.eq("userId", user.userId).eq("date", today)
|
||||
)
|
||||
.unique();
|
||||
|
||||
if (usage) {
|
||||
await ctx.db.patch(usage._id, {
|
||||
generationCount: usage.generationCount + 1,
|
||||
concurrentJobs: usage.concurrentJobs + 1,
|
||||
});
|
||||
} else {
|
||||
await ctx.db.insert("dailyUsage", {
|
||||
userId: user.userId,
|
||||
date: today,
|
||||
generationCount: 1,
|
||||
concurrentJobs: 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Verringert concurrentJobs um 1 (Minimum 0).
|
||||
* Nur aufrufen wenn INTERNAL_CREDITS_ENABLED !== "true"
|
||||
* (commit/release übernehmen das bei aktivierten Credits).
|
||||
*/
|
||||
export const decrementConcurrency = internalMutation({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const user = await requireAuth(ctx);
|
||||
const today = new Date().toISOString().split("T")[0];
|
||||
|
||||
const usage = await ctx.db
|
||||
.query("dailyUsage")
|
||||
.withIndex("by_user_date", (q) =>
|
||||
q.eq("userId", user.userId).eq("date", today)
|
||||
)
|
||||
.unique();
|
||||
|
||||
if (usage && usage.concurrentJobs > 0) {
|
||||
await ctx.db.patch(usage._id, {
|
||||
concurrentJobs: usage.concurrentJobs - 1,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user