mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 14:32:03 +00:00
fix(ai): every surface keeps a ceiling on the day's mixed total
The owner split left an operator-funded chain with no ceiling at all. Its arm compared operator_tokens, and that counter returns to roughly zero on every reconcile the user's own plan settled: on the 2026-09-11 evidence the shared key answered 500 and codex served, so the counter never grew, every job reservation was admitted forever, and a runaway client loop could write an unbounded row. A pre-existing concurrency contract caught it from the other side, two reservations that must not both pass one cap both passing. reserveBudget now refuses when the owner's counter is over its cap OR the day's mixed total is over the abuse ceiling, and a background surface gets its share of that ceiling too, so background spend is bounded on the total and not only under a counter the operator's key never fills. The surface is a required argument, so the type checker enumerated every reservation site. The hop guard learns the same lesson: an operator-funded fallback hop is now rationed at the calling surface's ceiling rather than the full operator cap. A job admitted under the job share could previously spend the whole 200 000 once its chain fell back onto the shared key, which is the original defect reached by the fallback path. The job share applies only where the operator pays for the chain. Multiplying whichever ceiling the chain resolved halved background generation for a self-hoster on a local model, who costs the operator nothing. Refusals now name the ceiling they hit and whose it was. A refusal used to emit the day's mixed total and nothing else, which on an operator refusal is not the counter that tripped.
This commit is contained in:
1 parent
ff51b72cd1
commit
18ed57b867
30 files changed
+645
-25
No files matched your search
@@ -119,6 +119,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
|
||||
providerType: provider.type === "admin-key" ? "admin-openai" : "local",
|
||||
},
|
||||
]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
annotate({ action: { name: "ai.test.budget_exceeded" } });
|
||||
|
||||
@@ -256,6 +256,7 @@ async function handleTextExtract(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
@@ -359,6 +360,7 @@ async function handleStoredExtract(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
@@ -479,6 +481,7 @@ async function handleVisionExtract(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
|
||||
@@ -251,6 +251,7 @@ async function handleVisionIndex(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
|
||||
@@ -168,6 +168,7 @@ async function handleTextSuggest(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
@@ -263,6 +264,7 @@ async function handleVisionSuggest(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
|
||||
@@ -347,6 +347,7 @@ async function handleTextSummary(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
@@ -438,6 +439,7 @@ async function handleVisionSummary(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
await refundDocumentAiSlot(userId);
|
||||
|
||||
@@ -326,6 +326,7 @@ describe("coach chat — tool-mode routing (F1)", () => {
|
||||
"2026-06-21",
|
||||
2_000_000,
|
||||
"user",
|
||||
"coach",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -569,6 +570,7 @@ describe("coach chat — tool-mode routing (F1)", () => {
|
||||
"2026-06-21",
|
||||
2_000_000,
|
||||
"user",
|
||||
"coach",
|
||||
);
|
||||
// The legacy path ships the snapshot figures in the user turn.
|
||||
const params = (
|
||||
|
||||
@@ -606,11 +606,25 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
|
||||
reqDateKey,
|
||||
dailyCap,
|
||||
resolveCostOwner(chain),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
// v1.38.19 (Wave E, fix round 1) — say WHICH ceiling refused and WHOSE.
|
||||
// `totalAfter` alone is the day's mixed total; on an operator refusal that
|
||||
// is not the counter that tripped, and reading `totalAfter: 1200000`
|
||||
// against a 200 000 ceiling is what turned the 06:42Z incident into a
|
||||
// production log dig. Integers and two small enums — no key, host or model
|
||||
// name is added.
|
||||
annotate({
|
||||
action: { name: "coach.budget.exceeded" },
|
||||
meta: { totalAfter: reservation.totalAfter },
|
||||
meta: {
|
||||
owner: reservation.owner,
|
||||
surface: "coach",
|
||||
limit: reservation.limit,
|
||||
cap: dailyCap,
|
||||
totalAfter: reservation.totalAfter,
|
||||
operatorAfter: reservation.operatorAfter,
|
||||
},
|
||||
});
|
||||
return streamProviderError({ code: "coach.budget.exceeded" });
|
||||
}
|
||||
@@ -748,6 +762,7 @@ async function handleChatRequest(request: NextRequest): Promise<Response> {
|
||||
// returned in full so every guard below still runs on the complete text.
|
||||
let streamedDeltas = 0;
|
||||
const fallback = await runStreamingRawCompletionWithFallback({
|
||||
surface: "coach",
|
||||
userId,
|
||||
providers: chain,
|
||||
onDelta: () => {
|
||||
|
||||
@@ -122,6 +122,8 @@ describe("POST /api/labs/ocr/extract — text mode budget", () => {
|
||||
200_000,
|
||||
// Wave E — and the cost owner that cap is enforced against as the 5th.
|
||||
"operator",
|
||||
// Wave E fix round 1 — the surface the ceiling is rationed for, 6th.
|
||||
"coach",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -185,6 +185,7 @@ async function handleTextExtract(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
annotate({
|
||||
@@ -289,6 +290,7 @@ async function handleVisionExtract(
|
||||
dateKey,
|
||||
resolveDailyCap([{ providerType: pick.entry.providerType }]),
|
||||
resolveCostOwner([{ providerType: pick.entry.providerType }]),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
annotate({
|
||||
|
||||
@@ -122,6 +122,7 @@ beforeEach(() => {
|
||||
totalAfter: 700,
|
||||
owner: "user",
|
||||
operatorAfter: 0,
|
||||
limit: null,
|
||||
});
|
||||
vi.mocked(resolveDailyCap).mockReturnValue(1_234_567);
|
||||
vi.mocked(resolveProviderChain).mockResolvedValue([
|
||||
@@ -279,6 +280,7 @@ describe("POST /api/medications/extract — daily budget", () => {
|
||||
"2026-05-28",
|
||||
1_234_567,
|
||||
"user",
|
||||
"coach",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -290,6 +292,7 @@ describe("POST /api/medications/extract — daily budget", () => {
|
||||
totalAfter: 9_999_999,
|
||||
owner: "user",
|
||||
operatorAfter: 0,
|
||||
limit: "owner-cap",
|
||||
});
|
||||
|
||||
const res = await POST(postReq({ text: "Mounjaro 5mg weekly" }) as never);
|
||||
|
||||
@@ -212,6 +212,7 @@ async function handleExtract(request: NextRequest): Promise<Response> {
|
||||
dateKey,
|
||||
resolveDailyCap(chain),
|
||||
resolveCostOwner(chain),
|
||||
"coach",
|
||||
);
|
||||
if (!reservation.allowed) {
|
||||
annotate({
|
||||
@@ -224,6 +225,7 @@ async function handleExtract(request: NextRequest): Promise<Response> {
|
||||
let completion;
|
||||
try {
|
||||
completion = await runRawCompletionWithFallback({
|
||||
surface: "coach",
|
||||
userId,
|
||||
providers: chain,
|
||||
params: singleUserTurn({
|
||||
|
||||
Reference in new issue
Block a user