fix(ai): count the daily AI budget on the account's own day

The token ledger keyed every day on UTC while the daily briefing and the
other per-day generations run on the account's local day, so east of UTC
the first generations after local midnight were refused against the
previous day's spent ceiling. The budget day is now the calendar day in
the account's time zone; each request still resolves it once and books
its reservation and reconcile on the same row. The medication extraction
reads relative phrases against the same local day, the operator's spend
card shows the operator's own day, and the limit copy says the budget
resets at midnight in the person's time zone.
This commit is contained in:
Marc-André Bombeck committed 2026-10-11 13:09:12 +02:00
1 parent 7d8ff0f2df
commit dcb7a0bc09
35 files changed
+175 -65

No files matched your search

+5 -3
View File
@@ -73,11 +73,13 @@ interface ProviderHealthSummary {
const CENTRAL_TYPES = [...OPERATOR_HELD_PROVIDER_TYPES];
export const GET = apiHandler(async () => {
await requireAdmin();
const { user } = 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();
// The ledger buckets each account by its own local day; the operator's
// "today" is the day in the operator's own zone, which on a single-zone
// instance is everyone's.
const dateKey = await buildDateKey(user.id);
const groups = await prisma.providerHealth.groupBy({
// `lastStatus` joins the grouping key rather than an aggregate: the
+1 -1
View File
@@ -145,7 +145,7 @@ export const POST = apiHandler(async (request: NextRequest) => {
// `admin-key` is the operator's own credential and draws the operator
// ceiling; an override names the caller's own key, and every other resolved
// type is the user's own egress, so those draw the user-plan ceiling.
const dateKey = buildDateKey();
const dateKey = await buildDateKey(user.id);
const reservation = await reserveBudget(
user.id,
AI_TEST_MAX_TOKENS,
@@ -183,7 +183,7 @@ async function reserveFor(
providerType: ProviderChainType,
tokens: number,
): Promise<AiRunBudget | null> {
const dateKey = buildDateKey();
const dateKey = await buildDateKey(userId);
const reservation = await reserveBudget(
userId,
tokens,
@@ -200,7 +200,7 @@ async function handleVisionIndex(
const rl = await checkDocumentAiRateLimit(userId);
if (!rl.allowed) return documentAiRateLimited(rl);
const dateKey = buildDateKey();
const dateKey = await buildDateKey(userId);
const reservation = await reserveBudget(
userId,
DOCUMENT_INDEX_RESERVE_TOKENS,
@@ -122,7 +122,7 @@ async function reserveFor(
userId: string,
providerType: ProviderChainType,
): Promise<AiRunBudget | null> {
const dateKey = buildDateKey();
const dateKey = await buildDateKey(userId);
const reservation = await reserveBudget(
userId,
AI_BUDGETS.documentAssist.maxTokens,
@@ -216,7 +216,7 @@ async function reserveFor(
ask: SummaryRequest,
providerType: ProviderChainType,
): Promise<AiRunBudget | null> {
const dateKey = buildDateKey();
const dateKey = await buildDateKey(ask.userId);
const reservation = await reserveBudget(
ask.userId,
budgetFor(ask.mode),
+2 -2
View File
@@ -157,7 +157,7 @@ async function handleTextExtract(
// v1.21.0 (F1) — the operator-cost cap applies only when the picked provider
// egresses on the operator's own key; a BYOK / Codex / local pick runs on the
// user's own plan and gets the generous user-plan ceiling.
const dateKey = buildDateKey();
const dateKey = await buildDateKey(userId);
const reservation = await reserveBudget(
userId,
OCR_RESERVE_TOKENS.text,
@@ -218,7 +218,7 @@ async function handleVisionExtract(
// v1.21.0 (F1) — the operator-cost cap applies only when the picked vision
// provider egresses on the operator's own key; a BYOK / Codex pick runs on
// the user's own plan and gets the generous user-plan ceiling.
const dateKey = buildDateKey();
const dateKey = await buildDateKey(userId);
const reservation = await reserveBudget(
userId,
OCR_RESERVE_TOKENS.vision,
+5 -2
View File
@@ -225,7 +225,10 @@ async function handleExtract(request: NextRequest): Promise<Response> {
chain.map((entry) => entry.providerType),
);
const today = todayOverride ?? buildDateKey();
// The account's own today: "tomorrow" in a description means the
// person's tomorrow, and the same key is the budget day below.
const accountToday = await buildDateKey(userId);
const today = todayOverride ?? accountToday;
const { systemPrompt, userPrompt } = buildMedicationExtractionPrompt({
text,
today,
@@ -239,7 +242,7 @@ async function handleExtract(request: NextRequest): Promise<Response> {
// paying for their own key by the operator's ceiling. `resolveDailyCap`
// charges the operator ceiling only when the chain's primary is the
// operator's own credential; every other chain gets the user-plan ceiling.
const dateKey = buildDateKey();
const dateKey = accountToday;
const estimatedTokens =
MAX_OUTPUT_TOKENS +
Math.ceil((systemPrompt.length + userPrompt.length) / 4);