fix(ai): charge what failed hops spent when a later hop answers

A chain whose first provider reasoned without answering and whose second
one answered billed only the answer: result.tokensUsed is the answering
call's alone, and the failed hop's spend, already reported on its
FallbackHop, was dropped by every caller that reconciles the day's ledger.

The raw runners now report tokensSpent on success too, the sum the failed
hops before the answer reported. The briefing, status, Coach (no-tools path
and every tool-loop round) and medication-extract reconciles charge it on
top of the answer. On failure, the Coach turn ledger's close keeps what the
failed chain reported spending instead of refunding everything, and the
medication-extract and document-chat routes charge it too, as the status
and briefing paths already did. Document indexing resolves a single provider
and already charged its failed call.

Refs #1279
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 00:33:18 +02:00
1 parent 186fc30fcc
commit b0ea3b1654
43 files changed
+198 -61

No files matched your search

@@ -1,7 +1,7 @@
{
"$comment": "Production functions over 200 lines, keyed \"file :: name\", and their length. Maintained by src/__tests__/source-size-ratchet.test.ts.",
"count": 351,
"total": 113530,
"count": 350,
"total": 113326,
"entries": {
"app/achievements/page.tsx :: AchievementsPage": 206,
"app/api/admin/backups/upload/route.ts :: apiHandler(arg)": 314,
@@ -31,7 +31,6 @@
"app/api/medications/[id]/dose-history/route.ts :: apiHandler(arg)": 256,
"app/api/medications/[id]/inventory/[itemId]/route.ts :: apiHandler(arg)": 204,
"app/api/medications/[id]/schedule-revisions/[revisionId]/route.ts :: apiHandler(arg)": 250,
"app/api/medications/extract/route.ts :: handleExtract": 201,
"app/api/mental-health/assessments/route.ts :: postAssessment": 249,
"app/api/mood-entries/[id]/route.ts :: apiHandler(arg) #2": 296,
"app/api/mood-entries/route.ts :: postMoodEntry": 324,
@@ -250,7 +249,7 @@
"lib/ai/coach/glp1-snapshot.ts :: buildGlp1SnapshotBlock": 218,
"lib/ai/coach/snapshot.ts :: buildCoachSnapshotImpl": 881,
"lib/ai/coach/tools/loop.ts :: runCoachToolLoop": 438,
"lib/ai/coach/turn/model.ts :: runTurnModel": 540,
"lib/ai/coach/turn/model.ts :: runTurnModel": 538,
"lib/ai/coach/turn/pipeline.ts :: runTurn": 274,
"lib/ai/coach/turn/reply-guards.ts :: guardReply": 331,
"lib/ai/codex-client.ts :: parseResponse": 302,
@@ -298,7 +297,7 @@
"lib/insights/comprehensive-generate.ts :: generateComprehensiveInsight": 876,
"lib/insights/features.ts :: extractFeatures": 796,
"lib/insights/mood-aggregates.ts :: fetchMoodAggregates": 246,
"lib/insights/status-provider.ts :: runStatusCompletion": 225,
"lib/insights/status-provider.ts :: runStatusCompletion": 224,
"lib/integrations/connection-health-load.ts :: loadConnectionFacts": 227,
"lib/integrations/status.ts :: recordSyncFailure": 292,
"lib/jobs/apple-health-import-worker.ts :: handleAppleHealthImport": 300,