Deterministic vs LLM Meal Planning: Why Our Planner Isn’t a Language Model
Cupboard’s planning engine is deterministic: the same pantry and the same configuration produce the same plan, every time. It scores each candidate recipe on 8 named factors with published weights, contains no random number generator and reads no clock of its own, and stores the full factor breakdown for every meal it picks. A language model offers none of that, and for deciding how you spend a grocery budget, those are the properties that matter.
This isn’t an argument that language models are bad. We use one, for something the engine could never do. It’s an argument about which subproblem gets which tool.
Deterministic engine vs language model, dimension by dimension
| Dimension | Deterministic engine | Language model |
|---|---|---|
| Reproducibility | Same inputs → identical output. Tests shuffle the input order and assert the scores match exactly (golden.test.ts:333-370) |
Non-deterministic by default; even at temperature 0, output drifts across model versions |
| Auditability | Each menu stores the engine version, injected clock, config and per-recipe factor scores (packages/domain/src/menu.ts:35-41) |
You can log the prompt and the response, but not recover why they produced that answer |
| Constraint handling | Set subtraction before scoring: excluded recipes never enter the pool (select.ts:32-33), unavailable-equipment ones are filtered in SQL (packages/db/src/queries.ts:110-126). A constraint can’t be outvoted |
Constraints share a channel with everything else in the prompt; a strong stylistic instruction can outweigh an allergy line |
| Failure when uncertain | Returns fewer meals than asked, naming the reason: pool_exhausted or all_infeasible (packages/menu-engine/src/types.ts:57-63) |
Fills the slot. An empty answer is out-of-distribution; a plausible one is not |
| Explanation of choices | 8 factor scores per selected recipe, weights published in source (config.ts:10-19) |
Post-hoc rationalisation, generated by the same process as the answer, not derived from it |
| Cost per plan | CPU only, no API call. The whole 41-test suite runs in about a second | Calls priced per token, scaling with the pantry and recipe pool in context |
| Flexibility of input | Typed struct: meal count, strict/flexible mode, servings per meal | Anything a person can say. Not a small advantage |
What “deterministic” means in this engine specifically
The planner is a pure function of one value. The clock is injected and never read internally (types.ts:32): the engine source contains no Math.random, no Date.now, and no argument-less new Date(). The only new Date calls in the package are frozen literals in fixtures.ts, giving the tests a fixed today.
The interesting part is the tiebreak. Scores are compared exactly, and ties resolve on lexicographic recipe ID (select.ts:20-23). An epsilon-fuzzed comparison was rejected because it wouldn’t be a total order, so the winner could depend on the order candidates arrived in — the bug that makes a system almost reproducible, which is worse than not claiming it.
The same instinct shows up in the shopping list. When two deficits for one ingredient won’t reconcile to a common unit — 100 g of butter and 1 cup, no density in the catalog — the engine keeps them as separate lines flagged unresolved instead of adding numbers that don’t add (shopping.ts:12-16). A tidier list is one guess away. It’s less tidy on purpose.
Four failure modes of LLM meal planners worth naming
All four follow from the same thing: a language model produces text that is likely, not text that is true.
Inventing ingredients you don’t own. The model has your pantry list in context and a strong prior about what a shakshuka contains. When those conflict, nothing guarantees the pantry wins.
Quietly dropping a constraint. “No shellfish” is a handful of tokens competing with everything else in the prompt. It usually survives. Usually is a bad standard for an allergy.
Different answers on Tuesday than Monday. Same pantry, same preferences, different plan — with no way to tell whether that’s variety or a mistake. If a user reports “it gave me a terrible week,” you cannot reproduce the terrible week.
Confident quantities. “You’ll need 400 g of chicken thighs” is a fluent sentence whether or not it corresponds to anything. A deficit is arithmetic — what a recipe requires minus what a pantry holds — and arithmetic is not what a language model does.
What language models are genuinely better at
Open-ended ideation. The engine picks from a corpus. It will never invent a dish or surprise you, because inventing things is precisely what we removed.
Natural-language input. “Something light, my in-laws are coming, one of them doesn’t eat dairy” is trivial for a model and impossible for a typed struct.
The long tail. Every factor has to be designed, weighted and shipped. A model handles requests nobody anticipated on day one.
Taste. Our variety handling is two subtractions: repeat protein costs 0.15, repeat cuisine 0.1 (factors.ts:161-172). That’s a crude proxy for “this week feels monotonous,” and a model has a far better sense of what a good week of eating looks like.
The one place Cupboard uses a model, and why
Reading a photograph of a shelf. You point your phone at the fridge and get an itemised inventory back, including best-by dates read off the labels (apps/api/src/vision.ts:59-77).
That is a computer vision problem. No deterministic algorithm turns a badly-lit photo of a fridge door into “oat milk, 1.75 L, best by 2026-08-22,” and refusing the model here for architectural purity would just ship a worse product.
It’s constrained hard at the boundary. Output is a JSON schema with additionalProperties: false and every field required. The prompt forbids inferring items that aren’t visible and forbids inventing amounts or dates — unreadable means null, not a guess — and each item carries a confidence. The parser has explicit branches for refusal, missing output, malformed JSON and schema drift (vision.ts:84-103), each surfacing as an error rather than a quietly empty pantry.
The pattern is: let the model perceive, don’t let it decide.
What determinism doesn’t buy you
Three caveats, because a comparison table that only cuts one way isn’t worth reading.
Reproducible isn’t the same as right. A deterministic engine can be consistently wrong, and ours has been. What you get is that when a plan is bad, you can open the factor breakdown, find the weight that caused it, and argue with a number instead of a vibe.
Our replay is partial. Provenance stores the engine version, the injected clock, the config and the factor scores — not a snapshot of the pantry. Any past menu can be explained and re-run under the exact configuration that produced it; bit-for-bit reproduction also needs that day’s inventory.
Dietary filtering isn’t wired yet. prefs.dietary exists on the user schema (packages/domain/src/user.ts:12), but the plan query today filters on QA-hidden recipes and missing equipment only. The architecture puts that filter upstream of scoring, where it can’t be outvoted. It isn’t connected yet. Don’t rely on it.
FAQ
Are AI meal planners reliable? For inspiration, yes. For deciding what to buy, they have a structural problem: a language model produces plausible text, and a shopping list needs to be correct. The specific risks are invented ingredients, dropped constraints, and quantities with no arithmetic behind them.
Why do AI meal planners make up recipes and ingredients? Because producing something that looks like a recipe is the task the model was trained on, and nothing separately checks the output against your pantry. If the model has seen a thousand recipes with fish sauce, fish sauce is a likely token whether or not you own any.
What’s the difference between a deterministic and an AI meal planner? A deterministic planner is an algorithm with published rules: identical inputs always produce the same plan, and you can see which factor drove each choice. An AI planner generates a plan from a prompt, may produce a different one tomorrow from the same inputs, and can only explain itself plausibly.
Does Cupboard use AI at all? Yes, in one place: interpreting pantry photos into structured items with expiry dates. That’s a perception problem where a model is the right tool. Everything downstream — which meals, which quantities, what to buy — is deterministic code.
Can a deterministic planner handle special requests? Poorly. It takes a meal count, a mode and servings per meal, and cannot handle “something light for guests.” A real cost of the design, not an oversight.
Every plan Cupboard generates comes back with a per-recipe score breakdown across all eight factors, and the weights behind it live in the source, not in a marketing claim. When a meal turns up that you don’t want, you can see whether expiry urgency, pantry coverage or a low shopping penalty put it there — and tell us which weight is wrong. Try it.
Last verified: August 2026. Every code reference was checked against the repository on 16 August 2026, and the 41-test engine suite was run to confirm the determinism claims. No third-party sources are cited in this post.
Related: I fact-checked my own marketing site · How to make a grocery list from what you have · What food goes bad the fastest