Update features and dependencies

This commit is contained in:
Arnaud
2026-07-01 11:10:37 +02:00
parent 9d9dfb46c6
commit 8b57a3fd87
107 changed files with 14654 additions and 458 deletions
+8 -6
View File
@@ -3,9 +3,10 @@ import { z } from "zod";
import { and, eq } from "@epicure/db";
import { db, recipes, recipeIngredients, recipeSteps } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { adaptRecipe } from "@/lib/ai/features/adapt-recipe";
import { withUserKey } from "@/lib/ai/resolve-user-key";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
const Schema = z.object({
excludeIngredients: z.array(z.string()).default([]),
@@ -42,9 +43,12 @@ export async function POST(req: NextRequest, { params }: Params) {
return NextResponse.json({ error: "Provide at least one constraint" }, { status: 400 });
}
await checkTierLimit(userId, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro", "aiCall");
const aiConfig = await withUserKey(userId, { provider: parsed.data.provider, model: parsed.data.model });
const [aiConfig, privateBio] = await Promise.all([
withUserKey(userId, { provider: parsed.data.provider, model: parsed.data.model }),
getUserPrivateBio(userId),
]);
const adapted = await adaptRecipe(
{
title: recipe.title,
@@ -57,12 +61,10 @@ export async function POST(req: NextRequest, { params }: Params) {
excludeIngredients: parsed.data.excludeIngredients,
extraConstraints: parsed.data.extraConstraints,
},
aiConfig,
{ ...aiConfig, userContext: privateBio ?? undefined },
(session!.user as { locale?: string }).locale ?? "en"
);
await incrementUsage(userId, "aiCall");
const newId = crypto.randomUUID();
const now = new Date();
+8 -6
View File
@@ -3,9 +3,10 @@ import { z } from "zod";
import { and, eq } from "@epicure/db";
import { db, recipes } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { suggestDrinks } from "@/lib/ai/features/suggest-drinks";
import { withUserKey } from "@/lib/ai/resolve-user-key";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
const Schema = z.object({
count: z.number().int().min(1).max(6).default(4),
@@ -33,9 +34,12 @@ export async function POST(req: NextRequest, { params }: Params) {
const parsed = Schema.safeParse(body ?? {});
if (!parsed.success) return NextResponse.json({ error: "Validation error" }, { status: 400 });
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const aiConfig = await withUserKey(session!.user.id, { provider: parsed.data.provider, model: parsed.data.model });
const [aiConfig, privateBio] = await Promise.all([
withUserKey(session!.user.id, { provider: parsed.data.provider, model: parsed.data.model }),
getUserPrivateBio(session!.user.id),
]);
const drinks = await suggestDrinks(
{
title: recipe.title,
@@ -45,11 +49,9 @@ export async function POST(req: NextRequest, { params }: Params) {
ingredients: recipe.ingredients,
},
parsed.data.count,
aiConfig,
{ ...aiConfig, userContext: privateBio ?? undefined },
(session!.user as { locale?: string }).locale ?? "en"
);
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json({ drinks });
}
@@ -0,0 +1,85 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { generateRecipe } from "@/lib/ai/features/generate-recipe";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
import { db, recipes, recipeIngredients, recipeSteps } from "@epicure/db";
import { parseQuantity } from "@/lib/parse-quantity";
const Schema = z.object({
title: z.string().min(1).max(200),
provider: z.enum(["openai", "anthropic", "openrouter", "ollama"]).optional(),
model: z.string().optional(),
});
export async function POST(req: NextRequest) {
const { session, response } = await requireSession();
if (response) return response;
const body = await req.json() as unknown;
const parsed = Schema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Validation error" }, { status: 400 });
}
const limited = await applyRateLimit(`rl:ai:${session!.user.id}`, 10, 60);
if (limited) return limited;
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const privateBio = await getUserPrivateBio(session!.user.id);
const recipe = await generateRecipe(parsed.data.title, {
provider: parsed.data.provider,
model: parsed.data.model,
userContext: privateBio ?? undefined,
});
const recipeId = crypto.randomUUID();
await db.insert(recipes).values({
id: recipeId,
authorId: session!.user.id,
title: recipe.title,
description: recipe.description ?? null,
baseServings: recipe.baseServings,
prepMins: recipe.prepMins ?? null,
cookMins: recipe.cookMins ?? null,
difficulty: recipe.difficulty ?? null,
visibility: "private",
aiGenerated: true,
language: "en",
dietaryTags: recipe.dietaryTags ?? {},
tags: [],
});
if (recipe.ingredients?.length) {
await db.insert(recipeIngredients).values(
recipe.ingredients.map((ing, i) => ({
id: crypto.randomUUID(),
recipeId,
rawName: ing.rawName,
quantity: parseQuantity(ing.quantity) ?? null,
unit: ing.unit ?? null,
note: ing.note ?? null,
order: i,
}))
);
}
if (recipe.steps?.length) {
await db.insert(recipeSteps).values(
recipe.steps.map((step, i) => ({
id: crypto.randomUUID(),
recipeId,
instruction: step.instruction,
timerSeconds: step.timerSeconds ?? null,
order: i,
}))
);
}
return NextResponse.json({ id: recipeId });
}
+8 -4
View File
@@ -2,12 +2,14 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { generateRecipe } from "@/lib/ai/features/generate-recipe";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
const Schema = z.object({
prompt: z.string().min(3).max(500),
language: z.string().max(10).default("en"),
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
provider: z.enum(["openai", "anthropic", "openrouter", "ollama"]).optional(),
model: z.string().optional(),
});
@@ -25,15 +27,17 @@ export async function POST(req: NextRequest) {
const limited = await applyRateLimit(`rl:ai:${session!.user.id}`, 10, 60);
if (limited) return limited;
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const privateBio = await getUserPrivateBio(session!.user.id);
const recipe = await generateRecipe(parsed.data.prompt, {
provider: parsed.data.provider,
model: parsed.data.model,
language: parsed.data.language,
difficulty: parsed.data.difficulty,
userContext: privateBio ?? undefined,
});
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json(recipe);
}
+10 -10
View File
@@ -3,7 +3,7 @@ import { z } from "zod";
import { db, recipes, recipeIngredients, recipeSteps } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit, TierLimitError } from "@/lib/tiers";
import { importFromPhoto } from "@/lib/ai/features/import-photo";
import { getModelConfigForUseCase } from "@/lib/ai/resolve-user-key";
@@ -25,16 +25,18 @@ export async function POST(req: NextRequest) {
const limited = await applyRateLimit(`rl:ai:${session!.user.id}`, 5, 60);
if (limited) return limited;
try {
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
} catch (err: unknown) {
const msg = err instanceof Error ? err.message : "Tier limit reached";
return NextResponse.json({ error: msg }, { status: 403 });
}
const userId = session!.user.id;
const locale = (session!.user as { locale?: string }).locale ?? "en";
try {
await checkAndIncrementTierLimit(userId, session!.user.tier as "free" | "pro", "aiCall");
} catch (err: unknown) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "AI call limit reached for your tier" }, { status: 403 });
}
throw err;
}
const aiConfig = await getModelConfigForUseCase(userId, "vision");
// Fall back to vision-capable defaults if no explicit model configured
@@ -51,8 +53,6 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: msg }, { status: 502 });
}
await incrementUsage(userId, "aiCall");
const newRecipeId = crypto.randomUUID();
const now = new Date();
+8 -4
View File
@@ -2,8 +2,9 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { importFromUrl } from "@/lib/ai/features/import-url";
import { validateWebhookUrl } from "@/lib/validate-webhook-url";
const Schema = z.object({
url: z.string().url(),
@@ -21,17 +22,20 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Validation error", issues: parsed.error.issues }, { status: 400 });
}
const ssrfError = await validateWebhookUrl(parsed.data.url);
if (ssrfError) {
return NextResponse.json({ error: ssrfError }, { status: 400 });
}
const limited = await applyRateLimit(`rl:ai:${session!.user.id}`, 10, 60);
if (limited) return limited;
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const recipe = await importFromUrl(parsed.data.url, {
provider: parsed.data.provider,
model: parsed.data.model,
});
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json(recipe);
}
@@ -5,6 +5,7 @@ import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { getDefaultProviderWithKey } from "@/lib/ai/resolve-user-key";
import { generateMealPlan } from "@/lib/ai/features/generate-meal-plan";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
const DAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] as const;
@@ -15,6 +16,7 @@ const Schema = z.object({
days: z.array(z.enum(DAYS)).min(1).max(7).default([...DAYS]),
usePantry: z.boolean().default(false),
pantryMode: z.boolean().default(false),
difficulty: z.enum(["easy", "medium", "hard"]).optional(),
});
export async function POST(req: NextRequest) {
@@ -32,7 +34,10 @@ export async function POST(req: NextRequest) {
const userId = session!.user.id;
const locale = (session!.user as { locale?: string }).locale ?? "en";
const config = await getDefaultProviderWithKey(userId);
const [config, privateBio] = await Promise.all([
getDefaultProviderWithKey(userId),
getUserPrivateBio(userId),
]);
// pantryMode forces usePantry on so pantry items are always fetched when maximizing pantry use
const effectiveUsePantry = parsed.data.usePantry || parsed.data.pantryMode;
@@ -54,8 +59,9 @@ export async function POST(req: NextRequest) {
pantryItems: pantryItemNames,
days: parsed.data.days,
pantryMode: parsed.data.pantryMode,
difficulty: parsed.data.difficulty,
},
config,
{ ...config, userContext: privateBio ?? undefined },
locale
);
@@ -94,7 +100,7 @@ export async function POST(req: NextRequest) {
id: crypto.randomUUID(),
recipeId,
rawName: ing.rawName,
quantity: ing.quantity ?? null,
quantity: ing.quantity != null ? String(ing.quantity) : null,
unit: ing.unit ?? null,
order: i,
}))
@@ -3,9 +3,10 @@ import { z } from "zod";
import { and, eq } from "@epicure/db";
import { db, recipes } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { suggestPairings } from "@/lib/ai/features/suggest-pairings";
import { withUserKey } from "@/lib/ai/resolve-user-key";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
const Schema = z.object({
count: z.number().int().min(1).max(6).default(4),
@@ -34,9 +35,12 @@ export async function POST(req: NextRequest, { params }: Params) {
const parsed = Schema.safeParse(body ?? {});
if (!parsed.success) return NextResponse.json({ error: "Validation error" }, { status: 400 });
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const aiConfig = await withUserKey(session!.user.id, { provider: parsed.data.provider, model: parsed.data.model });
const [aiConfig, privateBio] = await Promise.all([
withUserKey(session!.user.id, { provider: parsed.data.provider, model: parsed.data.model }),
getUserPrivateBio(session!.user.id),
]);
const pairings = await suggestPairings(
{
title: recipe.title,
@@ -46,11 +50,9 @@ export async function POST(req: NextRequest, { params }: Params) {
ingredients: recipe.ingredients,
},
parsed.data.count,
aiConfig,
{ ...aiConfig, userContext: privateBio ?? undefined },
(session!.user as { locale?: string }).locale ?? "en"
);
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json({ pairings });
}
@@ -0,0 +1,78 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { generateText } from "ai";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { getModelConfigForUseCase } from "@/lib/ai/resolve-user-key";
import { resolveModel } from "@/lib/ai/factory";
import { db, recipes, eq, and } from "@epicure/db";
import { getUserPrivateBio, buildUserBioContext } from "@/lib/ai/user-bio";
const Schema = z.object({
recipeId: z.string().uuid(),
question: z.string().min(1).max(500),
});
export async function POST(req: NextRequest) {
const { session, response } = await requireSession();
if (response) return response;
const body = await req.json() as unknown;
const parsed = Schema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Validation error" }, { status: 400 });
}
const limited = await applyRateLimit(`rl:ai:${session!.user.id}`, 30, 60);
if (limited) return limited;
const recipe = await db.query.recipes.findFirst({
where: and(eq(recipes.id, parsed.data.recipeId), eq(recipes.authorId, session!.user.id)),
with: {
ingredients: { orderBy: (t, { asc }) => asc(t.order) },
steps: { orderBy: (t, { asc }) => asc(t.order) },
},
});
if (!recipe) {
return NextResponse.json({ error: "Recipe not found" }, { status: 404 });
}
const ingredientList = recipe.ingredients
.map((ing) => [ing.quantity, ing.unit, ing.rawName, ing.note ? `(${ing.note})` : ""].filter(Boolean).join(" "))
.join("\n");
const stepList = recipe.steps
.map((s, i) => `${i + 1}. ${s.instruction}`)
.join("\n");
const recipeContext = `
Recipe: ${recipe.title}
${recipe.description ? `Description: ${recipe.description}` : ""}
Difficulty: ${recipe.difficulty ?? "unknown"}
Prep: ${recipe.prepMins ?? 0}min | Cook: ${recipe.cookMins ?? 0}min | Servings: ${recipe.baseServings}
INGREDIENTS:
${ingredientList || "None listed"}
STEPS:
${stepList || "None listed"}
`.trim();
const [config, privateBio] = await Promise.all([
getModelConfigForUseCase(session!.user.id, "text"),
getUserPrivateBio(session!.user.id),
]);
const model = resolveModel(config);
const bioContext = buildUserBioContext(privateBio);
const { text } = await generateText({
model,
system: `You are a helpful culinary assistant. Answer questions about the following recipe concisely and accurately. If a question is not related to the recipe or cooking, politely redirect. Keep answers under 200 words.
${recipeContext}${bioContext}`,
prompt: parsed.data.question,
});
return NextResponse.json({ answer: text });
}
@@ -0,0 +1,59 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { generateObject } from "ai";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { getModelConfigForUseCase } from "@/lib/ai/resolve-user-key";
import { resolveModel } from "@/lib/ai/factory";
import { getUserPrivateBio, buildUserBioContext } from "@/lib/ai/user-bio";
const InputSchema = z.object({
prompt: z.string().max(300).optional(),
});
const IdeasSchema = z.object({
ideas: z.array(z.object({
title: z.string(),
description: z.string(),
tags: z.array(z.string()).max(4),
difficulty: z.enum(["easy", "medium", "hard"]),
totalMins: z.number().int().optional(),
})).length(6),
});
export async function POST(req: NextRequest) {
const { session, response } = await requireSession();
if (response) return response;
const body = await req.json() as unknown;
const parsed = InputSchema.safeParse(body);
if (!parsed.success) {
return NextResponse.json({ error: "Validation error" }, { status: 400 });
}
const limited = await applyRateLimit(`rl:ai:${session!.user.id}`, 10, 60);
if (limited) return limited;
const [config, privateBio] = await Promise.all([
getModelConfigForUseCase(session!.user.id, "text"),
getUserPrivateBio(session!.user.id),
]);
const model = resolveModel(config);
const bioContext = buildUserBioContext(privateBio);
const userContext = bioContext
? `Consider the following user preferences when suggesting ideas:${bioContext}\n\n`
: "";
const prompt = parsed.data.prompt?.trim()
? `${userContext}Generate 6 diverse recipe ideas based on: "${parsed.data.prompt}". Include a mix of difficulty levels.`
: `${userContext}Generate 6 diverse, creative recipe ideas. Include different cuisines, difficulty levels, and meal types.`;
const { object } = await generateObject({
model,
schema: IdeasSchema,
prompt,
});
return NextResponse.json(object.ideas);
}
+2 -4
View File
@@ -4,7 +4,7 @@ import { db, recipes, eq, and, or } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { applyRateLimit } from "@/lib/rate-limit";
import { getDefaultProviderWithKey } from "@/lib/ai/resolve-user-key";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { scaleRecipe } from "@/lib/ai/features/scale-recipe";
const Schema = z.object({
@@ -40,7 +40,7 @@ export async function POST(req: NextRequest) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const aiConfig = await getDefaultProviderWithKey(session!.user.id);
@@ -56,7 +56,5 @@ export async function POST(req: NextRequest) {
(session!.user as { locale?: string }).locale ?? "en"
);
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json({ ingredients: scaledIngredients });
}
+13 -3
View File
@@ -1,7 +1,8 @@
import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { requireSession } from "@/lib/api-auth";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit, TierLimitError } from "@/lib/tiers";
import { applyRateLimit } from "@/lib/rate-limit";
import { substituteIngredient } from "@/lib/ai/features/substitute-ingredient";
const Schema = z.object({
@@ -19,7 +20,17 @@ export async function POST(req: NextRequest) {
const parsed = Schema.safeParse(body);
if (!parsed.success) return NextResponse.json({ error: "Validation error" }, { status: 400 });
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const rateLimitRes = await applyRateLimit(`ai:substitute:${session!.user.id}`, 10, 60);
if (rateLimitRes) return rateLimitRes;
try {
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
} catch (err) {
if (err instanceof TierLimitError) {
return NextResponse.json({ error: "AI call limit reached for your tier" }, { status: 403 });
}
throw err;
}
const context = parsed.data.recipeTitle
? `recipe "${parsed.data.recipeTitle}"`
@@ -31,6 +42,5 @@ export async function POST(req: NextRequest) {
{ provider: parsed.data.provider, model: parsed.data.model }
);
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json({ substitutions });
}
@@ -3,7 +3,7 @@ import { z } from "zod";
import { and, eq } from "@epicure/db";
import { db, recipes, recipeIngredients, recipeSteps } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { translateRecipe } from "@/lib/ai/features/translate-recipe";
const Schema = z.object({
@@ -35,7 +35,7 @@ export async function POST(req: NextRequest, { params }: Params) {
return NextResponse.json({ error: "Validation error", issues: parsed.error.issues }, { status: 400 });
}
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const translation = await translateRecipe(
{
@@ -48,8 +48,6 @@ export async function POST(req: NextRequest, { params }: Params) {
{ provider: parsed.data.provider, model: parsed.data.model }
);
await incrementUsage(session!.user.id, "aiCall");
// Save as new draft recipe
const newId = crypto.randomUUID();
const now = new Date();
@@ -3,9 +3,10 @@ import { z } from "zod";
import { and, eq } from "@epicure/db";
import { db, recipes } from "@epicure/db";
import { requireSession } from "@/lib/api-auth";
import { checkTierLimit, incrementUsage } from "@/lib/tiers";
import { checkAndIncrementTierLimit } from "@/lib/tiers";
import { suggestVariations } from "@/lib/ai/features/suggest-variations";
import { withUserKey } from "@/lib/ai/resolve-user-key";
import { getUserPrivateBio } from "@/lib/ai/user-bio";
const Schema = z.object({
count: z.number().int().min(1).max(5).default(3),
@@ -37,9 +38,12 @@ export async function POST(req: NextRequest, { params }: Params) {
return NextResponse.json({ error: "Validation error", issues: parsed.error.issues }, { status: 400 });
}
await checkTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
await checkAndIncrementTierLimit(session!.user.id, session!.user.tier as "free" | "pro", "aiCall");
const aiConfig = await withUserKey(session!.user.id, { provider: parsed.data.provider, model: parsed.data.model });
const [aiConfig, privateBio] = await Promise.all([
withUserKey(session!.user.id, { provider: parsed.data.provider, model: parsed.data.model }),
getUserPrivateBio(session!.user.id),
]);
const variations = await suggestVariations(
{
title: recipe.title,
@@ -48,12 +52,10 @@ export async function POST(req: NextRequest, { params }: Params) {
steps: recipe.steps,
},
parsed.data.count,
aiConfig,
{ ...aiConfig, userContext: privateBio ?? undefined },
parsed.data.directions,
(session!.user as { locale?: string }).locale ?? "en"
);
await incrementUsage(session!.user.id, "aiCall");
return NextResponse.json({ variations });
}