mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
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:
1 parent
186fc30fcc
commit
b0ea3b1654
43 files changed
+198
-61
No files matched your search
@@ -106,6 +106,7 @@ beforeEach(() => {
|
||||
result: { content: "ok", tokensUsed: 10, model: "m" },
|
||||
workingProvider: { providerType: "anthropic" },
|
||||
fallbackHops: [],
|
||||
tokensSpent: 0,
|
||||
} as never);
|
||||
});
|
||||
|
||||
@@ -194,6 +195,7 @@ describe("streamFencedReply — numeric grounding over the UNION", () => {
|
||||
},
|
||||
workingProvider: { providerType: "anthropic" },
|
||||
fallbackHops: [],
|
||||
tokensSpent: 0,
|
||||
} as never);
|
||||
const res = await streamFencedReply({
|
||||
...baseArgs,
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
AllProvidersFailedError,
|
||||
runStreamingRawCompletionWithFallback,
|
||||
} from "@/lib/ai/provider-runner";
|
||||
import { spentOnFailure } from "@/lib/ai/reasoning/answerless";
|
||||
import { singleUserTurn } from "@/lib/ai/types";
|
||||
import {
|
||||
buildDateKey,
|
||||
@@ -285,9 +286,8 @@ export async function streamFencedReply(
|
||||
args;
|
||||
|
||||
// ── Provider resolution: document order (local-first, codex last), SINGLE
|
||||
// pick, NO cascade — so the exact egress equals the checked target. The pick
|
||||
// re-checks `documentAi` for that provider, consent receipt included; a
|
||||
// refusal is thrown (403) before any egress. ──
|
||||
// pick, NO cascade, so the egress is the checked target. The pick re-checks
|
||||
// `documentAi` for it, receipt included, and throws 403 before egress. ──
|
||||
const { pick, withheld } = await resolveDocumentTextProvider(userId);
|
||||
if (withheld) throw withheld;
|
||||
if (!pick) {
|
||||
@@ -364,8 +364,7 @@ Reply now as the assistant, grounded ONLY in the documents above, in ${
|
||||
contractLocale === "de" ? "German" : "English"
|
||||
}.`;
|
||||
|
||||
// The authoritative grounding set: the LIVE attachments' texts only. Never
|
||||
// history, never a snapshot (none exists) — recomputed from `docs` this turn.
|
||||
// The grounding set: the LIVE attachments' texts only, recomputed this turn.
|
||||
const groundingSources = docs.map((d) => d.text);
|
||||
|
||||
type Outcome =
|
||||
@@ -397,7 +396,9 @@ Reply now as the assistant, grounded ONLY in the documents above, in ${
|
||||
});
|
||||
result = fallback.result;
|
||||
} catch (err) {
|
||||
await reconcileSpend(userId, reservation.reserved, 0, dateKey, 0, {
|
||||
// #1279 — what the failed call reported spending stays charged.
|
||||
const spent = spentOnFailure(err);
|
||||
await reconcileSpend(userId, reservation.reserved, spent, dateKey, 0, {
|
||||
servedBy: null,
|
||||
reservedOwner: reservation.owner,
|
||||
}).catch(() => {});
|
||||
|
||||
Reference in new issue
Block a user