PlatePilot needs a model in two places, both already isolated behind provider seams in packages/provider-contracts (ADR-0003):
| Seam | What it must do | Modality | Call shape |
|---|---|---|---|
MealAnalysisProvider |
Look at a sanitized plate photo and return the meal’s items with estimated nutrition (the ProviderJobResult shape). |
Image + text in | Asynchronous: submit → job id → getStatus. |
MealPlanProvider |
Produce a Monday–Sunday week of meals (title, method, per-serving nutrition, ingredients with store sections, allergens) from preferences; localize shopping wording. | Text in | Synchronous: generateWeek, localizeShoppingItems. |
The deterministic mocks (DeterministicMealAnalysisProvider, DeterministicMealPlanProvider) implement both today.
| Provider | Free offer (as far as verified) | Vision | Structured JSON output | Card needed | Notes |
|---|---|---|---|---|---|
| Google Gemini API (AI Studio) | Verified: the pricing page lists the Flash and Flash-Lite models (Gemini 3.8 Flash, 3.7/3.6/3.5 Flash, 3.5 and 3.1 Flash-Lite) as “Free of charge” on the free tier; Pro models are paid only. Per-model RPM/RPD limits are shown in AI Studio and on the rate-limits page (rendered client-side, so not captured here). | Yes | Yes: responseMimeType: application/json plus a responseSchema. |
No | One key covers both seams. Google states free-tier prompts and responses may be used to improve its products, so only synthetic data may go through it (ADR-0005). |
OpenRouter :free models |
Verified via GET /api/v1/models: 21 models with the :free suffix, several accepting image input (e.g. google/gemma-4-31b-it:free, nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free, qwen/qwen3.8-27b:free). |
Some | OpenAI-compatible response_format, model-dependent |
No | Aggregator; free models rotate, have low request-per-minute and per-day caps, and route to third-party hosts with their own data terms. Good as a fallback seam target because the API is OpenAI-shaped. |
| Groq | Verified: a “Free Plan” with per-model RPM/RPD/TPM limits exists on the rate-limits page. Model list on the free plan not captured; Groq has historically served Llama 4 Scout/Maverick (vision) and Llama 3.x there. | Yes (Llama 4) | OpenAI-compatible response_format: json_schema |
No | Very fast inference; best for the plan generator if Gemini limits bite. Vision quality is below Gemini Flash. |
| Cerebras | Verified: “New accounts receive $5 in free credits after adding a verified payment method”; a Free Trial tier with small RPM limits. | No | Yes | Yes | Text-only, so plan generation only. Card requirement rules it out for “free without commitment”. |
| Mistral (La Plateforme) | Not verified today: the public pricing page only shows the consumer chat plans. Mistral has had a free “Experiment” API tier with rate limits; Pixtral models support images. | Yes (Pixtral) | JSON mode | Historically no | Re-check the console before planning on it. |
| Hugging Face Inference Providers | Not verified today: free accounts have had a small monthly credit usable across hosted providers. | Some | Model-dependent | No | Credits are small; fine for experiments, not for a Sunday batch job across users. |
Use the Google Gemini API free tier for both seams, with Groq (plan generation) or an OpenRouter :free vision model (photo analysis) as the fallback adapter. Reasons:
@platepilot/contracts (GeneratedWeekPlan, NutritionMetrics, PlanIngredient).PLAN_AUTO_GENERATE_HOUR): cheap per request and the largest free daily quota; Flash for the interactive photo estimate.Trade-offs to accept:
429 as “retry later” rather than fail the week; the mock stays as the fallback when the quota is exhausted so the app never shows an empty week.provider.module.ts currently throws when MOCK_PROVIDERS=false. Record an ADR-0011 that allows a personal free-tier key for local exploration under three conditions: synthetic data only, key from the environment (never committed, never in .env.example with a value), and the mock remains the default.MEAL_PLAN_PROVIDER=mock|gemini, MEAL_ANALYSIS_PROVIDER=mock|gemini, GEMINI_API_KEY, GEMINI_PLAN_MODEL (default a Flash-Lite id), GEMINI_ANALYSIS_MODEL (default a Flash id) to apps/api/src/config/environment.ts with a refinement that requires the key when either selector is gemini, and keep the MOCK_PROVIDERS production guard.packages/provider-gemini (so the API and worker share them):
GeminiMealPlanProvider.generateWeek: one generateContent call per week with a system instruction built from the request (allergens as hard exclusions, dislikes, focus, dietary pattern, servings, unit system, location for product names), responseSchema generated from the contract zod schema, temperature low, and a post-validation pass that rejects any meal containing an excluded allergen or a disliked ingredient and re-asks for that slot only. localizeShoppingItems is a second, smaller call and can be skipped when location is absent.GeminiMealAnalysisProvider: submit records the job and returns queued; the worker’s processing step reads the sanitized object from LocalStack/S3, sends it inline (JPEG, already downscaled by the app) with a prompt asking for items, portions and the eight nutrition values, validates against ProviderJobResult, and maps refusals or malformed output to a retryable provider_failed. This keeps the existing outbox, lease and retry machinery unchanged.fetch rather than the SDK to keep the dependency surface small; both calls are a single POST.PlanWeek.generatedAt so the UI can say “sample plan”.GEMINI_API_KEY being present.docs/runbooks/provider-outage.md (quota exhaustion), and the threat model (third-party data flow, key handling).Google AI Studio → “Get API key” → create a key in a new Google Cloud project with billing left off. Keep it in the desktop’s shell environment or the repository .env (gitignored), never in apps/api/.env.example or a commit.