It's tempting to throw a nutrition question at an LLM and let it produce the numbers. The problem: language models are not calculators. They round unpredictably, occasionally invent coefficients, and can give two different answers to the same input.
For CalculCalories, a free French-language calorie calculator (BMR, TDEE, macros, protein, BMI, deficit and recipe nutrition), I used a strict split: the math runs in TypeScript; AI, when enabled, only comments on a result that has already been computed. Here's how that looks in practice.
Step 1: Deterministic formulas with named sources
Resting energy (BMR) uses Mifflin–St Jeor (1990):
type Sex = "female" | "male";
export function bmrMifflin(sex: Sex, kg: number, cm: number, age: number): number {
const base = 10 * kg + 6.25 * cm - 5 * age;
return sex === "male" ? base + 5 : base - 161;
}
Daily expenditure (TDEE) multiplies BMR by a physical activity level (PAL) factor:
export const PAL = {
sedentary: 1.2,
light: 1.375,
moderate: 1.55,
very: 1.725,
extreme: 1.9,
} as const;
export const tdee = (bmr: number, level: keyof typeof PAL) => bmr * PAL[level];
Every constant on the page is traceable to a named reference, so a user can redo the calculation by hand.
Step 2: Goals are adjustments to TDEE, not BMR
A common mistake is building a weight-loss target from BMR. The calculator applies the goal to TDEE, with a moderate deficit and a safety floor:
export function goalCalories(tdeeKcal: number, bmrKcal: number, goal: "lose" | "maintain" | "gain") {
const round10 = (n: number) => Math.round(n / 10) * 10;
if (goal === "maintain") return round10(tdeeKcal);
if (goal === "gain") return round10(tdeeKcal + 220);
const target = tdeeKcal * 0.8; // moderate deficit
return round10(Math.max(target, bmrKcal)); // never below resting needs
}
With the default example — 30-year-old woman, 165 cm, 62 kg, lightly active — BMR is about 1,340 kcal and TDEE about 1,843 kcal, rounded to 1,840 kcal for maintenance. The exact deficit/surplus values are a product choice; the point is they're explicit and testable.
Step 3: Macros are a sequence, not a split
Instead of a fixed "40/30/30" ratio, macros are derived in order: protein first (in g/kg), then a fat minimum, then carbs take the remaining calories.
export function macros(kcal: number, kg: number, proteinPerKg = 1.6) {
const protein = Math.round(kg * proteinPerKg);
const fat = Math.round((kcal * 0.28) / 9);
const carbs = Math.round((kcal - protein * 4 - fat * 9) / 4);
return { protein, fat, carbs };
}
Step 4: AI gets numbers in, words out
The optional "explain this result" button sends the already-computed values to a model and asks for a plain-language explanation. The model never receives the raw inputs as a problem to solve, and its output is displayed separately from the result:
const prompt = `
Explain these results in simple French for a non-expert.
Do not change or recompute any number.
BMR: ${bmr} kcal. TDEE: ${tdee} kcal (PAL ${pal}). Target: ${target} kcal.
Protein: ${m.protein} g, Fat: ${m.fat} g, Carbs: ${m.carbs} g.
`;
If the AI is disabled or fails, the calculator loses nothing — the explanation is a bonus, not the product.
Step 5: Be honest about limits
Health tools should say what they can't do. The page states that results are population estimates, that real needs vary with muscle mass, sleep, medication and health, and that it doesn't replace a doctor or dietitian. The food database is intentionally short and sourced, rather than thousands of generated pages.
Takeaways
- Put calculations in code with tests; let LLMs handle language.
- Name your formulas and sources on the page.
- Apply goals to the right baseline (TDEE, not BMR) and enforce floors.
- Make AI output optional and visually separate from the computed result.
The calculator is at calculcalories.net (in French). How are others drawing the line between deterministic code and LLM output in their apps?
Top comments (0)