mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
feat(admin): the AI readout separates operator-paid tokens
The provider-health endpoint showed delivery health only. When a surface refuses on budget the operator's next question is which ceiling he hit, and one mixed token figure cannot answer it: a day at 1.24 M tokens looks alarming until you see that 151 200 of them ran on the instance's own key and the rest on the users' plans. `spendToday` carries the day's key and both counters, aggregated across accounts. The operator ceiling is enforced against `operatorTokens`, so that is the figure the readout puts beside the total.
This commit is contained in:
1 parent
d3196ee69e
commit
b750e0bc7d
2 files changed
+74
-1
No files matched your search
@@ -18,6 +18,9 @@ vi.mock("@/lib/db", () => ({
|
||||
providerHealth: {
|
||||
groupBy: vi.fn(),
|
||||
},
|
||||
coachUsage: {
|
||||
aggregate: vi.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -45,6 +48,7 @@ import { prisma } from "@/lib/db";
|
||||
import { requireAdmin } from "@/lib/api-handler";
|
||||
|
||||
const groupBy = vi.mocked(prisma.providerHealth.groupBy);
|
||||
const aggregate = vi.mocked(prisma.coachUsage.aggregate);
|
||||
|
||||
function group(
|
||||
providerType: string,
|
||||
@@ -76,6 +80,9 @@ describe("GET /api/admin/provider-health", () => {
|
||||
vi.mocked(requireAdmin).mockResolvedValue({
|
||||
user: { id: "admin1" },
|
||||
} as never);
|
||||
aggregate.mockResolvedValue({
|
||||
_sum: { totalTokens: 0, operatorTokens: 0 },
|
||||
} as never);
|
||||
});
|
||||
|
||||
it("is admin-gated before it touches the ledger", async () => {
|
||||
@@ -199,6 +206,49 @@ describe("GET /api/admin/provider-health", () => {
|
||||
).toEqual(["admin-openai", "admin-codex", "anthropic", "local"]);
|
||||
});
|
||||
|
||||
/**
|
||||
* v1.38.19 (Wave E) — the two spend figures side by side.
|
||||
*
|
||||
* The operator reads this card when the chat refuses. Until now it showed
|
||||
* delivery health only, and the day's token figure was one number that mixed
|
||||
* his own ChatGPT plan with the instance's key — so a refusal at 513 539
|
||||
* tokens looked like his own doing. The split is what tells him which
|
||||
* ceiling he hit.
|
||||
*/
|
||||
it("reports the day's spend split by who paid for it", async () => {
|
||||
groupBy.mockResolvedValue([] as never);
|
||||
aggregate.mockResolvedValue({
|
||||
_sum: { totalTokens: 1_240_000, operatorTokens: 151_200 },
|
||||
} as never);
|
||||
|
||||
const res = await GET();
|
||||
const body = await res.json();
|
||||
|
||||
expect(body.data.spendToday).toEqual({
|
||||
dateKey: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/),
|
||||
totalTokens: 1_240_000,
|
||||
operatorTokens: 151_200,
|
||||
});
|
||||
// Today only, across every account — the operator's ceiling is per day.
|
||||
expect(aggregate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
where: { dateKey: expect.stringMatching(/^\d{4}-\d{2}-\d{2}$/) },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reports a day with no spend as zero, not null", async () => {
|
||||
groupBy.mockResolvedValue([] as never);
|
||||
aggregate.mockResolvedValue({
|
||||
_sum: { totalTokens: null, operatorTokens: null },
|
||||
} as never);
|
||||
|
||||
const res = await GET();
|
||||
const body = await res.json();
|
||||
expect(body.data.spendToday.totalTokens).toBe(0);
|
||||
expect(body.data.spendToday.operatorTokens).toBe(0);
|
||||
});
|
||||
|
||||
it("answers an empty ledger with an empty list, not an error", async () => {
|
||||
groupBy.mockResolvedValue([] as never);
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiHandler, requireAdmin } from "@/lib/api-handler";
|
||||
import { buildDateKey } from "@/lib/ai/coach/budget";
|
||||
import { prisma } from "@/lib/db";
|
||||
import { apiSuccess } from "@/lib/api-response";
|
||||
import { annotate } from "@/lib/logging/context";
|
||||
@@ -49,6 +50,9 @@ export const GET = apiHandler(async () => {
|
||||
await requireAdmin();
|
||||
annotate({ action: { name: "admin.provider-health.get" } });
|
||||
|
||||
// The UTC day the ledger buckets by — the same key every reservation writes.
|
||||
const dateKey = buildDateKey();
|
||||
|
||||
const groups = await prisma.providerHealth.groupBy({
|
||||
// `lastStatus` joins the grouping key rather than an aggregate: the
|
||||
// MAX of two HTTP statuses is not a status, it is arithmetic on a
|
||||
@@ -107,5 +111,24 @@ export const GET = apiHandler(async () => {
|
||||
return a.providerType.localeCompare(b.providerType);
|
||||
});
|
||||
|
||||
return apiSuccess({ providers });
|
||||
// v1.38.19 (Wave E) — the day's spend, split by who pays for it.
|
||||
//
|
||||
// The operator opens this card when a surface refuses on budget. One mixed
|
||||
// number could not answer the question it raised: a day at 1.24 M tokens is
|
||||
// alarming until you see that 151 200 of them were the instance's own key
|
||||
// and the rest ran on the users' plans. The operator ceiling is enforced
|
||||
// against the second figure, so both belong on screen.
|
||||
const spend = await prisma.coachUsage.aggregate({
|
||||
where: { dateKey },
|
||||
_sum: { totalTokens: true, operatorTokens: true },
|
||||
});
|
||||
|
||||
return apiSuccess({
|
||||
providers,
|
||||
spendToday: {
|
||||
dateKey,
|
||||
totalTokens: spend._sum.totalTokens ?? 0,
|
||||
operatorTokens: spend._sum.operatorTokens ?? 0,
|
||||
},
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user