Files
HealthLog/src/lib/ai/ai-budgets.ts
T
Marc-André Bombeck fb54b46034 fix(labs): reserve a proportionate budget for text-mode OCR
Text-mode OCR structures browser-extracted text rather than running a
vision call, so it reserved and reconciled the full vision budget even
though the spend is far lower. Add an ocrExtractText ceiling and reserve
that in text mode, and refund the reservation in full on a clean extract
failure instead of charging the reserved estimate.
2026-06-26 09:26:47 +02:00

125 lines
5.4 KiB
TypeScript

/**
* Per-surface AI generation budgets — the single source of truth for the
* `temperature` and `maxTokens` each AI surface sends to the provider.
*
* Before this module the values were scattered as `?? 0.3` / `?? 1000`
* magic numbers across the status provider, the Coach route, the
* comprehensive generator and the period-narrative generator. Centralising
* them keeps the cost ceiling auditable in one place and ties every number
* to the output contract it serves (commented per entry below).
*
* `seed` (where present) is a deterministic constant pinned for the
* reference/reproducible surfaces (per the v1.18.7 AI audit MEDIUM-4):
* the status cards and the period narrative are reference assessments, so a
* fixed seed makes a prompt change diff-able against a stable baseline. The
* daily-briefing paragraph is deliberately NOT seeded (it re-rolls for
* phrasing variety — see comprehensive-generate.ts).
*/
export interface AiBudget {
/** Sampling temperature. Low for reference surfaces, slightly higher for chat. */
temperature: number;
/**
* Output-token ceiling. Sized to the surface's documented output contract.
* Optional for surfaces whose caller pins its own token const (self-context).
*/
maxTokens?: number;
}
/**
* Deterministic seed pinned for the reproducible reference surfaces. A stable
* constant (not a per-request value) so two runs of the same prompt+snapshot
* on a seed-aware provider produce byte-identical text — the QA-attribution
* baseline the audit's MEDIUM-4 calls for.
*/
export const REFERENCE_AI_SEED = 1_618_033 as const;
/**
* Surface budgets. Each entry's comment states the output contract that
* fixes its `maxTokens`.
*/
export const AI_BUDGETS = {
/**
* Comprehensive insight + dailyBriefing — the largest structured payload
* (summary + recommendations + dailyBriefing 80-200 words + signals +
* keyFindings + trendAnnotations + storyboardAnnotations). 1500 tokens.
*/
comprehensive: { temperature: 0.3, maxTokens: 1500 },
/**
* Per-metric status assessment cards — output is a single
* `{ "summary": "..." }` of 2-4 sentences / 30-60 words
* (base-system.ts `length` section). ~80 tokens of content + JSON
* envelope; 250 is a comfortable ceiling (was an over-generous 1000).
*/
status: { temperature: 0.3, maxTokens: 250 },
/**
* Batched per-metric status assessment (v1.18.7 HIGH-1) — ONE call that
* returns a `{ perMetric: { bp, weight, pulse, bmi, mood, compliance,
* general } }` envelope, each value a single 30-60-word assessment in the
* same contract as a standalone status card. The seven warm calls a fully
* instrumented account fired per cycle collapse into this one. Sized for
* up to seven ~80-token summaries plus the JSON envelope: 1400 tokens.
* Temperature matches the per-card 0.45 (cadence entropy while the FACTS
* stay pinned by the snapshot + the forbidden-phrase guards).
*/
statusBatch: { temperature: 0.45, maxTokens: 1400 },
/**
* Period narrative (week/month) — 2-4 short plain-text sentences
* (period-narrative-generate.ts SYSTEM_PROMPT). 400 tokens.
*/
narrative: { temperature: 0.3, maxTokens: 400 },
/**
* Coach SSE chat — a 60-180 word prose reply plus an optional evidence
* sentinel block (system-prompt.ts rule 1). 600 tokens. Slightly higher
* temperature than the reference surfaces for conversational variety.
*/
coach: { temperature: 0.4, maxTokens: 600 },
/**
* Coach rolling-conversation summary worker — a compact summary of the
* elided older turns. 200 tokens.
*/
summary: { temperature: 0.3, maxTokens: 200 },
/**
* Coach durable-fact extraction worker — a short structured fact list.
* 300 tokens. Lowest temperature: extraction must be faithful, not
* creative.
*/
facts: { temperature: 0.2, maxTokens: 300 },
/**
* Coach self-context question generation — a small set of clarifying
* questions. Temperature only; the caller pins its own token const.
*/
selfContext: { temperature: 0.4 },
/**
* v1.18.9 — Lab-OCR structured extraction. ONE vision call transcribes a
* photo / PDF of a paper lab report into a JSON array of analyte rows
* (analyte + value/valueText + unit + reference range + date + per-field
* confidence). A full panel is a dozen-plus rows of compact JSON; 4000
* tokens covers a dense report plus the envelope. Temperature 0 — the task
* is faithful transcription, not generation, so the lowest setting minimises
* invented values. Vision input tokens are expensive, so the route also
* gates with a tight 6/hour rate bucket + reserveBudget + upload-size cap.
*/
ocrExtract: { temperature: 0, maxTokens: 4000 },
/**
* v1.20.1 — Lab-OCR text-mode structuring. The browser OCR's the image
* (tesseract.js) and POSTs only the extracted TEXT, so the provider does a
* plain text→JSON structuring pass — no expensive vision input tokens. The
* output contract is identical (the same dozen-plus analyte rows), but the
* spend ceiling is far lower than the vision path's 4000, so the day-budget
* reservation should reflect that rather than over-charging a text call at
* the vision rate. 1500 tokens comfortably covers a dense report's JSON
* envelope. Temperature 0 — faithful structuring, not generation.
*/
ocrExtractText: { temperature: 0, maxTokens: 1500 },
} as const satisfies Record<string, AiBudget>;