You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A locked spec for adding OpenAI as a third first-party AI provider, alongside the direct Anthropic client and the OpenRouter broker, covering both AI features — reading a meal from text and from a photo. Every decision settled and written down, so a build session can execute it without reopening a question. Building is not part of this map.
Notes
Domain. Flutter/Dart, Clean Architecture, lib/features/add_meal. Repo conventions in AGENTS.md. Sessions should reach for /grilling and /domain-modeling; research tickets are resolved by a research subagent.
This map reverses a decision, and that is its first ticket. OpenAI has been ruled out twice — once in #599's decision #7, once in #656 — and the ruling is quoted in source: AiModelCatalogue's doc comment says the all-Anthropic list is "a consequence rather than a preference" and names OpenAI's minors clause as the cause. Nothing downstream of that ticket is worth doing until it resolves. If the ruling holds, this map closes after one cheap ticket rather than after a build.
The two guarantees must survive a third provider. Both are structural rather than documentary, and that is the point of them:
the tool schema has no macro fields, so a model has nowhere to put a calorie count — decision Develop #5, settled 2026-08-14: a model may never emit nutrition values;
from a photo a model may return a count, never a measurement — any amount arriving with a unit is discarded along with its number.
The seam already exists, and it is OpenAI-shaped.MealItemsApi is provider-neutral, and OpenRouterMealItemsApi is explicitly built on "the OpenAI-compatible shape" — messages array with system first, tools wrapped in a function object, tool_choice as a named function, arguments returned as a JSON string, images as data URIs. A direct OpenAI client is a much smaller delta than Anthropic→OpenRouter was: endpoint, no provider routing block, no metadata header, no pin check. Whether that delta is a third sibling class or a shared dialect base is a ticket, not a foregone conclusion.
The known technical friction.mealItemsToolSchema declares required: ['query'] with quantity and unit optional. OpenAI's strict mode requires every property to appear in required. strict is not sent today, so this is a design choice rather than a blocker — but it is the one place where the shared schema might have to change for all three providers at once.
What is at stake beyond code. The README stakes a falsifiable privacy claim — "these four destinations, nothing else" — and its table names Anthropic and OpenRouter with a sentence explaining that they are not equivalent. Four provider-specific ARB keys × nine locales carry provider names in user-visible text. The settings dialog's model rows are already tight at two lines on a real phone.
There are artifacts outside the repo, and they are already behind. The two iubenda privacy policies (EN and DE) were never updated for OpenRouter — #670 settled what they must say and the manual editing never happened. Both still declare five recipients, none of them Open Food Facts, Supabase, Anthropic or OpenRouter, while the README calls them the formal policy. A third provider compounds an existing gap rather than opening a new one.
Scoping decided while charting. OpenAI is a first-party provider, reached directly. Both existing providers stay. Both AI features are in scope.
Decisions so far
Does OpenAI's usage policy permit a BYO-key nutrition tracker? — a qualified yes. All three clauses are conduct tests the app does not meet, and Anthropic's own disordered-eating prohibition is broader than OpenAI's, so it cannot be what separates the vendors — Do OpenRouter's terms preserve the policy fit Anthropic was chosen for? #656 conflated two different rejections. Google's is a distribution test; OpenAI has none. The bound party is the key holder, i.e. the user. But OpenAI offers no carve-out, only the absence of a prohibition, so the defence is factual ("the app gives no advice") and dies the day a feature says something evaluative. Two prior notes corrected: §2.2 permits the opposite shape and §3.3(g) is a prohibition — split out as its own ticket.
Does BYO-key fall foul of the API-key transfer prohibition? — §3.3(g) is not the obstacle, and OpenAI's own help centre proves it by telling users how to vet third-party tools that request their key. But the Does OpenAI's usage policy permit a BYO-key nutrition tracker? #679 precedent argument is unavailable: neither Anthropic nor OpenRouter has an equivalent clause, so the project has never accepted this restriction. Consistency survives on custody instead — Anthropic's guidance is verbally stricter and was shipped against. The real ambiguity is now Service terms §1 binding the API reference's "don't expose it in any client-side code such as browsers or apps", which the keystore answers on substance but not on text. Escalated to a support enquiry.
What does OpenAI do with prompts and images? — no quotable sentence exists. "Ephemeral" appears once on OpenAI's data page, about Code Interpreter. The README needs two sentences, both weaker: training is parity, retention is 30 days with a "harm" escape hatch — and two OpenAI pages disagree, so quote the weaker. ZDR is unreachable for an individual, so the app must promise the default and never mention it. §6 is not engaged. Identity beats the OpenRouter path (safety_identifier optional). WebP confirmed. Surfaced a fourth destination — Cloudflare, plus human moderation vendors — split out as its own ticket.
Does the tool schema stay optional-field, or adopt OpenAI strict mode? — unchanged, and strict: false is sent deliberately — omitting it normalizes into strict and would 400 every request, so doing nothing is the dangerous option. The prior reasoning was wrong (a nullable union is sanctioned), but strict buys nothing: the no-macros guarantee lives in _mealItemFrom, and validateParsedMealItems handles a bad unit better than a refusal would. Generalised into a standing rule — the app never relies on provider-side constrained decoding; every guarantee must be checkable in Dart against a response that ignored the schema.
Where does the failure taxonomy live at three providers? — each client classifies its own statuses; the exception carries a meaning, not a number. The neutral vocabulary already existed in the use cases — only the status-to-meaning step was misplaced. The four getters go; statusCode survives as a diagnostic. OpenAI's 404/422/403 disagreements then cost nothing. Vocabulary gains billing (one string × 9 locales) because both folds are actively wrong — "retry forever" or "check a key that works"; geo-block folds into unsupported, which already advises switching provider. Constrains A third sibling client, or a shared OpenAI-dialect base? #685.
How do the credential store and provider enum absorb a third member? — slots stay, but what is stored becomes visible: at three the app can hold three billing credentials while the dialog shows one, and removing a non-active provider's key needs you to switch to it first. And an unrecognised provider tag stops meaning Anthropic — absent still defaults (the no-migration property), but an unknown name makes the feature quietly unavailable rather than silently redirecting a downgraded user to a company they never chose. The tag is not cleared, so re-upgrading restores the choice.
Does the README's destination enumeration survive a sub-processor chain? — a destination is a contracting party, sub-processors are stated once as a class, and human review is named. Cloudflare is infrastructure and almost certainly already true of Open Food Facts and Supabase; people reading your dinner is a different claim and is what the table is consulted for. The row ships for all three providers or none — only OpenAI's vendors are known, and publishing those alone would penalise the provider with better disclosure. Resolves the claude-on-aws fog: Amazon is Anthropic's sub-processor, so the pin's guarantee is about contract, not hardware.
What human review does each provider disclose, and by whom? — "named vendors / none named / none named", and the shape is the point: all three publish sub-processor lists, and Anthropic's and OpenRouter's carry no content-review vendor at all, so OpenAI's TaskUs and Accenture are better disclosure, not worse practice. Anthropic's reassuring "no personnel can read" sentence is fenced to Covered Models and cannot be borrowed for haiku-4-5/sonnet-5; what is API-scoped is flagged content retained up to 2 years. OpenRouter adds a router, not a reviewer. Surfaced a defect in shipped text — the README's ephemerality quote omits that exception — filed as README: the photo ephemerality quote omits its exception #704.
Not yet specified
Anthropic's white paper Security and Privacy Design of Anthropic Data Retention and Review is public but will not render for an automated fetch. It is the document most likely to sharpen the Anthropic human-review row, and needs ten minutes in a real browser rather than another agent.
Whether three providers still fit the settings dialog as a list, or want a different shape. The model row title already needed hand-tuning to stay inside two lines at a real phone's density; a prototype may be the way in once the strings are settled.
Cost and rate-limit surfacing across three providers with three different billing models.
Whether the text and photo prompts need per-model tuning for OpenAI's range, or one prompt holds.
How the offline test discipline — live corpus, mutation checks, fixture labels — scales to a third provider without tripling every suite.
Out of scope
Reaching OpenAI models through OpenRouter (only: ["openai"] on a catalogue entry). Ruled out while charting: far cheaper, but it buys a different thing — two destinations instead of one, the forwarded account identity, and OpenRouter's weaker retention promise. This map is for a first-party provider.
Tier 1a / 2a — model-emitted macros. Settled and closed; a third provider does not reopen it.
Routing the existing Anthropic path through any broker. Ruled out on the previous map and unchanged.
Destination
A locked spec for adding OpenAI as a third first-party AI provider, alongside the direct Anthropic client and the OpenRouter broker, covering both AI features — reading a meal from text and from a photo. Every decision settled and written down, so a build session can execute it without reopening a question. Building is not part of this map.
Notes
Domain. Flutter/Dart, Clean Architecture,
lib/features/add_meal. Repo conventions inAGENTS.md. Sessions should reach for/grillingand/domain-modeling; research tickets are resolved by a research subagent.This map reverses a decision, and that is its first ticket. OpenAI has been ruled out twice — once in #599's decision #7, once in #656 — and the ruling is quoted in source:
AiModelCatalogue's doc comment says the all-Anthropic list is "a consequence rather than a preference" and names OpenAI's minors clause as the cause. Nothing downstream of that ticket is worth doing until it resolves. If the ruling holds, this map closes after one cheap ticket rather than after a build.The two guarantees must survive a third provider. Both are structural rather than documentary, and that is the point of them:
The seam already exists, and it is OpenAI-shaped.
MealItemsApiis provider-neutral, andOpenRouterMealItemsApiis explicitly built on "the OpenAI-compatible shape" — messages array with system first,toolswrapped in afunctionobject,tool_choiceas a named function, arguments returned as a JSON string, images as data URIs. A direct OpenAI client is a much smaller delta than Anthropic→OpenRouter was: endpoint, noproviderrouting block, no metadata header, no pin check. Whether that delta is a third sibling class or a shared dialect base is a ticket, not a foregone conclusion.The known technical friction.
mealItemsToolSchemadeclaresrequired: ['query']withquantityandunitoptional. OpenAI's strict mode requires every property to appear inrequired.strictis not sent today, so this is a design choice rather than a blocker — but it is the one place where the shared schema might have to change for all three providers at once.What is at stake beyond code. The README stakes a falsifiable privacy claim — "these four destinations, nothing else" — and its table names Anthropic and OpenRouter with a sentence explaining that they are not equivalent. Four provider-specific ARB keys × nine locales carry provider names in user-visible text. The settings dialog's model rows are already tight at two lines on a real phone.
There are artifacts outside the repo, and they are already behind. The two iubenda privacy policies (EN and DE) were never updated for OpenRouter — #670 settled what they must say and the manual editing never happened. Both still declare five recipients, none of them Open Food Facts, Supabase, Anthropic or OpenRouter, while the README calls them the formal policy. A third provider compounds an existing gap rather than opening a new one.
Scoping decided while charting. OpenAI is a first-party provider, reached directly. Both existing providers stay. Both AI features are in scope.
Decisions so far
Does OpenAI's usage policy permit a BYO-key nutrition tracker? — a qualified yes. All three clauses are conduct tests the app does not meet, and Anthropic's own disordered-eating prohibition is broader than OpenAI's, so it cannot be what separates the vendors — Do OpenRouter's terms preserve the policy fit Anthropic was chosen for? #656 conflated two different rejections. Google's is a distribution test; OpenAI has none. The bound party is the key holder, i.e. the user. But OpenAI offers no carve-out, only the absence of a prohibition, so the defence is factual ("the app gives no advice") and dies the day a feature says something evaluative. Two prior notes corrected: §2.2 permits the opposite shape and §3.3(g) is a prohibition — split out as its own ticket.
Does BYO-key fall foul of the API-key transfer prohibition? — §3.3(g) is not the obstacle, and OpenAI's own help centre proves it by telling users how to vet third-party tools that request their key. But the Does OpenAI's usage policy permit a BYO-key nutrition tracker? #679 precedent argument is unavailable: neither Anthropic nor OpenRouter has an equivalent clause, so the project has never accepted this restriction. Consistency survives on custody instead — Anthropic's guidance is verbally stricter and was shipped against. The real ambiguity is now Service terms §1 binding the API reference's "don't expose it in any client-side code such as browsers or apps", which the keystore answers on substance but not on text. Escalated to a support enquiry.
What does OpenAI do with prompts and images? — no quotable sentence exists. "Ephemeral" appears once on OpenAI's data page, about Code Interpreter. The README needs two sentences, both weaker: training is parity, retention is 30 days with a "harm" escape hatch — and two OpenAI pages disagree, so quote the weaker. ZDR is unreachable for an individual, so the app must promise the default and never mention it. §6 is not engaged. Identity beats the OpenRouter path (
safety_identifieroptional). WebP confirmed. Surfaced a fourth destination — Cloudflare, plus human moderation vendors — split out as its own ticket.Which OpenAI API, and what does its wire format demand? — Responses, with
store: falseon every request. Not for storage — What does OpenAI do with prompts and images, and what survives of the README's claims? #680's chat-completions inference was wrong, both endpoints store by default — but because tool calling is unsupported in Chat Completions withreasoning: nonefrom GPT-5.4, so a one-shot tokenisation would pay reasoning tokens per meal line. Only three things transfer from the OpenRouter client, which kills the shared-dialect option in A third sibling client, or a shared OpenAI-dialect base? #685. The failure taxonomy does not port at all — split out. And the nullable union is sanctioned, so Does the tool schema stay optional-field, or adopt OpenAI strict mode? #683 has a real option:_mealItemFromalready maps explicit null to null.Does the tool schema stay optional-field, or adopt OpenAI strict mode? — unchanged, and
strict: falseis sent deliberately — omitting it normalizes into strict and would 400 every request, so doing nothing is the dangerous option. The prior reasoning was wrong (a nullable union is sanctioned), but strict buys nothing: the no-macros guarantee lives in_mealItemFrom, andvalidateParsedMealItemshandles a bad unit better than a refusal would. Generalised into a standing rule — the app never relies on provider-side constrained decoding; every guarantee must be checkable in Dart against a response that ignored the schema.Where does the failure taxonomy live at three providers? — each client classifies its own statuses; the exception carries a meaning, not a number. The neutral vocabulary already existed in the use cases — only the status-to-meaning step was misplaced. The four getters go;
statusCodesurvives as a diagnostic. OpenAI's 404/422/403 disagreements then cost nothing. Vocabulary gainsbilling(one string × 9 locales) because both folds are actively wrong — "retry forever" or "check a key that works"; geo-block folds intounsupported, which already advises switching provider. Constrains A third sibling client, or a shared OpenAI-dialect base? #685.How do the credential store and provider enum absorb a third member? — slots stay, but what is stored becomes visible: at three the app can hold three billing credentials while the dialog shows one, and removing a non-active provider's key needs you to switch to it first. And an unrecognised provider tag stops meaning Anthropic — absent still defaults (the no-migration property), but an unknown name makes the feature quietly unavailable rather than silently redirecting a downgraded user to a company they never chose. The tag is not cleared, so re-upgrading restores the choice.
Does the README's destination enumeration survive a sub-processor chain? — a destination is a contracting party, sub-processors are stated once as a class, and human review is named. Cloudflare is infrastructure and almost certainly already true of Open Food Facts and Supabase; people reading your dinner is a different claim and is what the table is consulted for. The row ships for all three providers or none — only OpenAI's vendors are known, and publishing those alone would penalise the provider with better disclosure. Resolves the
claude-on-awsfog: Amazon is Anthropic's sub-processor, so the pin's guarantee is about contract, not hardware.What human review does each provider disclose, and by whom? — "named vendors / none named / none named", and the shape is the point: all three publish sub-processor lists, and Anthropic's and OpenRouter's carry no content-review vendor at all, so OpenAI's TaskUs and Accenture are better disclosure, not worse practice. Anthropic's reassuring "no personnel can read" sentence is fenced to Covered Models and cannot be borrowed for
haiku-4-5/sonnet-5; what is API-scoped is flagged content retained up to 2 years. OpenRouter adds a router, not a reviewer. Surfaced a defect in shipped text — the README's ephemerality quote omits that exception — filed as README: the photo ephemerality quote omits its exception #704.Not yet specified
Anthropic's white paper Security and Privacy Design of Anthropic Data Retention and Review is public but will not render for an automated fetch. It is the document most likely to sharpen the Anthropic human-review row, and needs ten minutes in a real browser rather than another agent.
Whether three providers still fit the settings dialog as a list, or want a different shape. The model row title already needed hand-tuning to stay inside two lines at a real phone's density; a prototype may be the way in once the strings are settled.
Cost and rate-limit surfacing across three providers with three different billing models.
Whether the text and photo prompts need per-model tuning for OpenAI's range, or one prompt holds.
How the offline test discipline — live corpus, mutation checks, fixture labels — scales to a third provider without tripling every suite.
Out of scope
only: ["openai"]on a catalogue entry). Ruled out while charting: far cheaper, but it buys a different thing — two destinations instead of one, the forwarded account identity, and OpenRouter's weaker retention promise. This map is for a first-party provider.