From 5f640e142066dadc4c8a8d88d4ec79578ee1bfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 26 Jun 2026 14:08:35 +0200 Subject: [PATCH] refactor(coach): collapse the duplicate daily-cap export to OPERATOR_COST_CAP The operator-cost cap carried two export names for one value; keep the accurate one (it gates only the operator-key path) and drop the alias. --- src/lib/ai/coach/__tests__/budget.test.ts | 11 ++++++----- src/lib/ai/coach/budget.ts | 12 +++--------- src/lib/ai/coach/self-context-questions.ts | 4 ++-- tests/integration/coach-chat.test.ts | 6 +++--- 4 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/lib/ai/coach/__tests__/budget.test.ts b/src/lib/ai/coach/__tests__/budget.test.ts index 267cb1dcf..b07cc00ce 100644 --- a/src/lib/ai/coach/__tests__/budget.test.ts +++ b/src/lib/ai/coach/__tests__/budget.test.ts @@ -3,7 +3,6 @@ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; import { HttpError } from "@/lib/api-handler"; import { buildDateKey, - MAX_TOKENS_PER_USER_PER_DAY, OPERATOR_COST_CAP, USER_PLAN_CAP, resolveDailyCap, @@ -76,7 +75,7 @@ describe("budget", () => { it("over-budget throws HttpError(429)", async () => { prismaMock.coachUsage.findUnique.mockResolvedValue({ - totalTokens: MAX_TOKENS_PER_USER_PER_DAY, + totalTokens: OPERATOR_COST_CAP, }); const { enforceBudget } = await import("../budget"); await expect(enforceBudget("user-1", "2026-05-10")).rejects.toMatchObject({ @@ -233,11 +232,13 @@ describe("reconcileSpend cached-token subtraction (F3)", () => { }); }); -describe("MAX_TOKENS_PER_USER_PER_DAY (F2 — reasoning-aware operator cap)", () => { +describe("OPERATOR_COST_CAP (F2 — reasoning-aware operator cap)", () => { it("is sized for reasoning turns, not a single non-reasoning reply", () => { // Was 25_000 (≈ one gpt-5.x reasoning turn). Raised so the operator-key // path survives a normal day of reasoning turns. - expect(MAX_TOKENS_PER_USER_PER_DAY).toBeGreaterThanOrEqual(150_000); - expect(OPERATOR_COST_CAP).toBe(MAX_TOKENS_PER_USER_PER_DAY); + expect(OPERATOR_COST_CAP).toBeGreaterThanOrEqual(150_000); + // The user-plan cap is far more generous — a user's own egress is never + // gated on the operator-cost ceiling. + expect(USER_PLAN_CAP).toBeGreaterThan(OPERATOR_COST_CAP); }); }); diff --git a/src/lib/ai/coach/budget.ts b/src/lib/ai/coach/budget.ts index f52f52340..0fc0a3704 100644 --- a/src/lib/ai/coach/budget.ts +++ b/src/lib/ai/coach/budget.ts @@ -34,13 +34,7 @@ import type { ProviderChainType } from "@/lib/ai/provider-chain"; * surgery) while leaving room for a normal day of reasoning turns on the * operator's key. The cap stays a real ceiling on the operator's exposure. */ -export const MAX_TOKENS_PER_USER_PER_DAY = 200_000; - -/** - * v1.21.0 (F1) — historical name retained for any external reference; the - * operator-cost cap is the only cap the ledger enforces. - */ -export const OPERATOR_COST_CAP = MAX_TOKENS_PER_USER_PER_DAY; +export const OPERATOR_COST_CAP = 200_000; /** * v1.21.0 (F1) — the daily ceiling for a chain whose egress runs on the @@ -108,7 +102,7 @@ export async function getDailyTokenSpend( export async function enforceBudget( userId: string, dateKey: string = buildDateKey(), - cap: number = MAX_TOKENS_PER_USER_PER_DAY, + cap: number = OPERATOR_COST_CAP, ): Promise { const spent = await getDailyTokenSpend(userId, dateKey); if (spent >= cap) { @@ -148,7 +142,7 @@ export async function reserveBudget( userId: string, estimatedTokens: number, dateKey: string = buildDateKey(), - cap: number = MAX_TOKENS_PER_USER_PER_DAY, + cap: number = OPERATOR_COST_CAP, ): Promise { const reserved = Number.isFinite(estimatedTokens) && estimatedTokens > 0 diff --git a/src/lib/ai/coach/self-context-questions.ts b/src/lib/ai/coach/self-context-questions.ts index da93def96..d6a4074cc 100644 --- a/src/lib/ai/coach/self-context-questions.ts +++ b/src/lib/ai/coach/self-context-questions.ts @@ -26,7 +26,7 @@ import { singleUserTurn } from "@/lib/ai/types"; import { buildDateKey, getDailyTokenSpend, - MAX_TOKENS_PER_USER_PER_DAY, + OPERATOR_COST_CAP, recordSpend, } from "@/lib/ai/coach/budget"; import { @@ -137,7 +137,7 @@ export async function deriveClarifyingQuestions( // Budget gate — same per-day ledger the Coach chat enforces. const dateKey = buildDateKey(); const spent = await getDailyTokenSpend(userId, dateKey); - if (spent >= MAX_TOKENS_PER_USER_PER_DAY) return fallback(); + if (spent >= OPERATOR_COST_CAP) return fallback(); const provider = await resolveProvider(userId); // v1.16.6 — same snapshot the Coach chat rides; lets the model diff --git a/tests/integration/coach-chat.test.ts b/tests/integration/coach-chat.test.ts index 508b4e78d..c0fd6c4e2 100644 --- a/tests/integration/coach-chat.test.ts +++ b/tests/integration/coach-chat.test.ts @@ -24,7 +24,7 @@ process.env.ENCRYPTION_KEY ??= import { cookieJar, headerJar } from "./mock-next-headers"; import { getPrismaClient, truncateAllTables } from "./setup"; -import { MAX_TOKENS_PER_USER_PER_DAY } from "@/lib/ai/coach/budget"; +import { OPERATOR_COST_CAP } from "@/lib/ai/coach/budget"; vi.mock("next/headers", async () => { const { cookieJar, headerJar } = await import("./mock-next-headers"); @@ -227,7 +227,7 @@ describe("POST /api/insights/chat — integration", () => { data: { userId, dateKey, - totalTokens: MAX_TOKENS_PER_USER_PER_DAY, + totalTokens: OPERATOR_COST_CAP, messageCount: 50, }, }); @@ -251,7 +251,7 @@ describe("POST /api/insights/chat — integration", () => { const usage = await prisma.coachUsage.findUnique({ where: { userId_dateKey: { userId, dateKey } }, }); - expect(usage?.totalTokens).toBe(MAX_TOKENS_PER_USER_PER_DAY); + expect(usage?.totalTokens).toBe(OPERATOR_COST_CAP); }); it("returns 404 (not 403) when conversationId belongs to another user", async () => {