feat(ai): Vercel AI SDK provider factory with BYOK and per-use-case model prefs

Provider factory (OpenAI/Anthropic/OpenRouter/Ollama). generateObject for all outputs.
Features: recipe generation, photo-to-recipe vision, variations, drink/meal pairings,
ingredient substitution, recipe adaptation, nutrition analysis, URL import, meal plan gen.
User BYOK keys (AES-256-GCM). Per-use-case model preferences (text/vision/mealPlan).
Site-level key override via admin settings.
This commit is contained in:
Arnaud
2026-07-01 08:10:19 +02:00
parent 84d6cfeb07
commit d9d58fd01a
26 changed files with 1656 additions and 0 deletions
@@ -0,0 +1,48 @@
import { generateObject } from "ai";
import { z } from "zod";
import { resolveModel, type AiConfig } from "../factory";
const PairingsOutputSchema = z.object({
pairings: z.array(z.object({
name: z.string(),
role: z.enum(["starter", "side", "salad", "bread", "drink", "dessert", "sauce"]),
description: z.string(),
whyItPairs: z.string(),
difficulty: z.enum(["easy", "medium", "hard"]),
prepTimeMins: z.number().int().optional(),
})),
});
export type PairingSuggestion = z.infer<typeof PairingsOutputSchema>["pairings"][number];
const LANG: Record<string, string> = { en: "English", fr: "French" };
export async function suggestPairings(
recipe: {
title: string;
description?: string | null;
difficulty?: string | null;
dietaryTags?: Record<string, boolean> | null;
ingredients: Array<{ rawName: string }>;
},
count = 4,
config?: AiConfig,
locale?: string
): Promise<PairingSuggestion[]> {
const model = resolveModel(config);
const lang = LANG[locale ?? "en"] ?? "English";
const dietaryContext = recipe.dietaryTags
? Object.entries(recipe.dietaryTags).filter(([, v]) => v).map(([k]) => k).join(", ")
: "";
const { object } = await generateObject({
model,
schema: PairingsOutputSchema,
system:
`You are an expert chef and menu planner. Suggest complementary dishes that form a balanced, cohesive complete meal. Consider flavor profiles, cooking techniques, dietary restrictions, and cultural harmony. Avoid duplicating ingredients heavily. Suggest a balanced mix of roles (starter, sides, dessert, drinks). Respond in ${lang}.`,
prompt: `Suggest ${count} dishes that pair well with this recipe to form a complete meal:\n\nRecipe: ${recipe.title}\n${recipe.description ? `Description: ${recipe.description}` : ""}\n${dietaryContext ? `Dietary: ${dietaryContext}` : ""}\nKey ingredients: ${recipe.ingredients.slice(0, 8).map((i) => i.rawName).join(", ")}`,
});
return object.pairings;
}