The number the AI was never allowed to guess.
How we added meal recognition to a health app without letting a model's guess become a health fact.
The guess that looked right
Low Ox Life had an old photo-analysis path in its code. Nobody could reach it from the app, but the idea inside it was plain: take a picture of a meal, let a model identify the food, ask it for an oxalate figure, and add the result to the journal.
Where it couldn't match a food, the model filled the gap with numbers like 40, 15, 5 and 10 milligrams. They looked reasonable. That was the problem.
A ridiculous number invites correction. A plausible health figure invites trust.
The screen could have looked polished and the answer could have arrived fast, and the system would still have been wrong. So the question changed: how do we use AI to reduce the work without letting its interpretation become health fact? The answer was not a disclaimer. It was architecture.
The job people were actually doing
Low Ox Life helps people understand and track dietary oxalate, often after a clinician has talked to them about kidney stones. It carries food data from a published Harvard T.H. Chan School table (the app is independent of Harvard), plus personal lists, recipes, meal planning, symptoms, reports, widgets and Apple Watch.
Looking up one food is easy. A plate is not one food. A salad might hold spinach, cheese, almonds, tomatoes and dressing, and logging it means breaking it apart: each food needs a name, a match, a portion and a source. People were doing that translation by hand.
That was the opportunity. Not "put AI in the app", and not a food chatbot: help someone move from a meal they can see to a record the system can stand behind. Translation was a good job for AI. Health judgment was not.
The obvious solution failed the product
The fastest design was a single call: send the photo to a vision model, ask for the foods, portions and oxalate figures, and show the answer. It failed for three reasons.
No sense of authority
The model had no way to know which source deserved to be trusted.
Guesses beside tested values
An estimate could land in the journal next to measured figures, and the screen would make them look the same.
Unknown became a number
The app already had a rule this broke: unknown is not zero. An untested food isn't oxalate-free, and missing evidence can't quietly become the safest-looking value.
The flaw didn't live on the result screen. It lived upstream, where the model's output became information, and no amount of interface polish could fix that.
We separated recognition from measurement
AI may name foods and estimate amounts. Low Ox Life supplies every oxalate figure.
That rule lives in the shape of the system. The model can return a food name, up to three alternate names, an amount, a household unit, a gram estimate and a confidence level. It can't return oxalate, calories, calcium, protein or any other nutrient, because those fields don't exist in its response schema. A test fails if one ever appears.
A prompt is an instruction. A schema is a boundary. A model can ignore an instruction, but it can't put a figure into a field the system doesn't accept. We stopped asking the model to behave responsibly and took the irresponsible action out of the contract.

The model prepares the work
Preparation, not delegation.
Meal recognition starts from Today. A Pro member can take a photo, choose up to three photos of one meal, or describe the meal by typing or dictating, with an optional note for what a picture can’t show.
Before anything leaves the phone, the app checks the person's access and AI consent, and loads only the foods they've allowed the AI to see. Seeing those names, the model answers in words the app already understands. When personal lists can't be shared, the server falls back to the public Harvard names.
The photo is compressed on the phone and sent to our own server, never straight to the model provider. The server checks the request size first, then sign-in, subscription, consent and rate limits, and only then asks the vision model. It cleans the reply, caps it at 15 foods, and logs counts, never what someone ate.
At that point the AI's part is finished. Nothing has entered the journal, and nothing has become a health figure.
A recognized food is not a matched food
The model might say "spinach, raw". That doesn’t make a food record true. The app still decides whether a record in its catalogue is the same food, under one narrow rule: only an unambiguous exact match on a food’s primary name can select itself.
Alternate names and near matches suggest candidates, but neither decides. When several exact records disagree on the figure, the app treats it as ambiguous and asks. It prefers Harvard records, then cooking staples, then the person’s own lists, and offers up to five candidates.
Recognition produces a draft, not a record. On the review screen the person can replace a food, search beyond the candidates, change a portion, remove an item, change the meal or the time, or leave an unmatched food untested. The button says exactly what it does.
This isn't ceremonial review. The person settles what each food is and how much, before the draft becomes a record. The AI doesn't approve its own work, and the person doesn't inherit an answer they can't see into. Friction stays only where it protects meaning.

Unknown survived the whole system
Some foods won't match a tested record, and the system doesn't force them to. An unmatched food stays untested, and the screen shows a question mark, never zero.
That carries on past the review. A day that includes an untested food isn't shown as a complete total; it reads “at least”. Reports keep the same distinction, and some averages use only fully tested days. The unknown holds through recognition, matching, review, storage, daily totals and reports.
The question mark isn't decoration. It shows a truth the system kept all the way down.


Failure can't erase the person's work
AI fails in ordinary ways. Each one is handled on its own terms.
Retry only what can change
An unusable reply gets one retry. A rate limit doesn't, because asking again only spends another request.
No food isn't an outage
A photo with no food in it says so, and a provider failure says something different.
No catalogue, no verdict
If the food catalogue can't load, the flow stops. A failed lookup isn't evidence that no match exists.
Partial saves stay recoverable
Each food saves on its own. Saved rows leave the review, the rest stay editable, and each keeps its ID, so a retry can’t create a duplicate.
Consent is enforced, not just asked
Meal photos and descriptions can hold personal and health information, so recognition needs the person's AI data-access consent, which they can change later in Settings. The server checks the account's consent record before it contacts any provider, and refuses when consent is missing.
Photos are checked by their actual bytes and dimensions, not by the file type they claim. Prompt fields are cleaned, and the model's reply is validated, bounded and stripped of links.
Consent without an enforced boundary is only a promise.
The decisions that hold it together
None of them is a screen. Together they decide what the screens can truthfully say.
AI can't generate oxalate or nutrient values
The response schema has no field for one.
Unknown is not zero
An untested food shows a question mark, and the day reads “at least”.
Only exact primary names select themselves
Everything else is a candidate the person picks.
The person confirms before anything is saved
Recognition makes a draft, never a record.
Provider access needs explicit AI consent
Checked on the server, not only on the phone.
Partial failures stay recoverable
Saved rows leave; the rest retry without duplicates.
What we can prove, and what we can't yet
The app's tests cover the schema rule, matching, security, unknown values, partial saves and duplicate prevention. They prove the system follows its rules. They don't prove that people understand those rules, or that the feature pays for itself, so we won't claim that here.
Next we measure how long a mixed meal takes to log, how often people correct a match or a portion, how often a match is wrong, and whether people who use it keep coming back. If it doesn’t help people, stay safe and support the business at the same time, it hasn’t worked.
The model was never the whole experience. The experience included the catalogue, the source, the match, the person’s judgment, the journal, and a report produced weeks later that someone might carry into a conversation with their clinician. We didn’t need AI to know the answer. We needed it to prepare the work without pretending to know more than it did.
The most important decision wasn't which model to use. It was what the model would never be allowed to become.


Want this kind of care on your back office?
The same rules apply to your setup: AI prepares the work, and you decide what counts.
