Files
food-ai/backend/internal/gemini/menu.go
dbastrikin 7241802931 fix: split menu generation into 3 parallel recipe calls to avoid Groq 403
Requesting 21 full recipes in one prompt exceeds the token budget and
returns 403 Forbidden. Replace the single-call approach with three
concurrent GenerateRecipes calls (breakfast ×7, lunch ×7, dinner ×7),
then assemble the 7-day plan from the results.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-22 12:05:48 +02:00

98 lines
2.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gemini
import (
"context"
"fmt"
"sync"
)
// MenuRequest contains parameters for weekly menu generation.
type MenuRequest struct {
UserGoal string
DailyCalories int
Restrictions []string
CuisinePrefs []string
AvailableProducts []string
}
// DayPlan is the AI-generated plan for a single day.
type DayPlan struct {
Day int `json:"day"`
Meals []MealEntry `json:"meals"`
}
// MealEntry is a single meal within a day plan.
type MealEntry struct {
MealType string `json:"meal_type"` // breakfast | lunch | dinner
Recipe Recipe `json:"recipe"`
}
// GenerateMenu produces a 7-day × 3-meal plan by issuing three parallel
// GenerateRecipes calls (one per meal type). This avoids token-limit errors
// that arise from requesting 21 full recipes in a single prompt.
func (c *Client) GenerateMenu(ctx context.Context, req MenuRequest) ([]DayPlan, error) {
type mealSlot struct {
mealType string
fraction float64 // share of daily calories
}
slots := []mealSlot{
{"breakfast", 0.25},
{"lunch", 0.40},
{"dinner", 0.35},
}
type mealResult struct {
recipes []Recipe
err error
}
results := make([]mealResult, len(slots))
var wg sync.WaitGroup
for i, slot := range slots {
wg.Add(1)
go func(idx int, mealType string, fraction float64) {
defer wg.Done()
// Scale daily calories to what this meal should contribute.
mealCal := int(float64(req.DailyCalories) * fraction)
r, err := c.GenerateRecipes(ctx, RecipeRequest{
UserGoal: req.UserGoal,
DailyCalories: mealCal * 3, // prompt divides by 3 internally
Restrictions: req.Restrictions,
CuisinePrefs: req.CuisinePrefs,
Count: 7,
AvailableProducts: req.AvailableProducts,
})
results[idx] = mealResult{r, err}
}(i, slot.mealType, slot.fraction)
}
wg.Wait()
for i, res := range results {
if res.err != nil {
return nil, fmt.Errorf("generate %s: %w", slots[i].mealType, res.err)
}
if len(res.recipes) == 0 {
return nil, fmt.Errorf("no %s recipes returned", slots[i].mealType)
}
// Pad to exactly 7 by repeating the last recipe.
for len(results[i].recipes) < 7 {
results[i].recipes = append(results[i].recipes, results[i].recipes[len(results[i].recipes)-1])
}
}
days := make([]DayPlan, 7)
for day := range 7 {
days[day] = DayPlan{
Day: day + 1,
Meals: []MealEntry{
{MealType: slots[0].mealType, Recipe: results[0].recipes[day]},
{MealType: slots[1].mealType, Recipe: results[1].recipes[day]},
{MealType: slots[2].mealType, Recipe: results[2].recipes[day]},
},
}
}
return days, nil
}