From 66a33b0a07fce06a11d1cb5ee7caa3e58fceedeb Mon Sep 17 00:00:00 2001 From: joaovictor91123 Date: Fri, 26 Jun 2026 06:00:26 -0400 Subject: [PATCH] fix(ai-summaries): resolve the shared daily neuron budget like the AI review path (10M) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit summarizeAgentBundleWithAi and rewriteSignalBundleWithAi resolved AI_DAILY_NEURON_BUDGET with a 10k default and a 1M ceiling, while ai-review.ts and ai-slop.ts (#1369) resolve the SAME shared daily counter to a 10M default and 10M ceiling. Because all three Workers-AI features sum into one sumAiEstimatedNeuronsSince total, summaries were starved into quota_exceeded once shared usage crossed 10k neurons — a tiny fraction of the real budget — and a configured budget was capped at 1M. Mirror the sibling resolution (default 10M, finite-check, clamp to 10M) at both sites and add regression tests pinning the high default, the raised ceiling, and the invalid-to-default fallback for both the summarize and rewrite paths. --- src/services/ai-summaries.ts | 14 +++++++++-- test/unit/ai-summaries.test.ts | 43 ++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/src/services/ai-summaries.ts b/src/services/ai-summaries.ts index db8b0221fd..7386f95f8b 100644 --- a/src/services/ai-summaries.ts +++ b/src/services/ai-summaries.ts @@ -33,7 +33,12 @@ export async function summarizeAgentBundleWithAi(env: Env, bundle: AgentRunBundl const signalBundle = compactAgentSignalBundle(bundle, visibility); const prompt = buildPrompt(signalBundle, visibility); const estimatedNeurons = estimateNeurons(prompt, maxOutputTokens); - const budget = clampNumber(Number(env.AI_DAILY_NEURON_BUDGET || 10000), 0, 1_000_000); + // Resolve the SHARED daily neuron budget exactly like ai-review.ts / ai-slop.ts (#1369): all three + // Workers-AI features sum into one `sumAiEstimatedNeuronsSince` counter, so the old `|| 10000` default + + // 1M ceiling here starved summaries into quota_exceeded once shared usage crossed 10k — well under the + // real 10M shared budget — and capped a configured budget at 1M. Default HIGH (10M) and clamp to 10M. + const rawNeuronBudget = Number(env.AI_DAILY_NEURON_BUDGET); + const budget = clampNumber(env.AI_DAILY_NEURON_BUDGET && Number.isFinite(rawNeuronBudget) ? rawNeuronBudget : 10_000_000, 0, 10_000_000); const used = await sumAiEstimatedNeuronsSince(env, utcDayStartIso()); const remainingBudget = Math.max(0, budget - used); @@ -274,7 +279,12 @@ export async function rewriteSignalBundleWithAi(env: Env, req: AiRewriteRequest) const maxOutputTokens = clampNumber(Number(env.AI_MAX_OUTPUT_TOKENS || 256), 64, 512); const prompt = buildBundlePrompt(req.bundle, req.visibility); const estimatedNeurons = estimateNeurons(prompt, maxOutputTokens); - const budget = clampNumber(Number(env.AI_DAILY_NEURON_BUDGET || 10000), 0, 1_000_000); + // Resolve the SHARED daily neuron budget exactly like ai-review.ts / ai-slop.ts (#1369): all three + // Workers-AI features sum into one `sumAiEstimatedNeuronsSince` counter, so the old `|| 10000` default + + // 1M ceiling here starved summaries into quota_exceeded once shared usage crossed 10k — well under the + // real 10M shared budget — and capped a configured budget at 1M. Default HIGH (10M) and clamp to 10M. + const rawNeuronBudget = Number(env.AI_DAILY_NEURON_BUDGET); + const budget = clampNumber(env.AI_DAILY_NEURON_BUDGET && Number.isFinite(rawNeuronBudget) ? rawNeuronBudget : 10_000_000, 0, 10_000_000); const used = await sumAiEstimatedNeuronsSince(env, utcDayStartIso()); const remainingBudget = Math.max(0, budget - used); diff --git a/test/unit/ai-summaries.test.ts b/test/unit/ai-summaries.test.ts index a48c270b1b..060c12549b 100644 --- a/test/unit/ai-summaries.test.ts +++ b/test/unit/ai-summaries.test.ts @@ -7,6 +7,7 @@ import { } from "../../src/services/ai-summaries"; import type { AgentRunBundle } from "../../src/services/agent-orchestrator"; import { FORBIDDEN_PUBLIC_COMMENT_WORDS } from "../../src/queue-intelligence"; +import { recordAiUsageEvent } from "../../src/db/repositories"; import { createTestEnv } from "../helpers/d1"; const PUBLIC_FORBIDDEN_TEXT = @@ -107,8 +108,8 @@ describe("Workers AI summaries", () => { expect(lowTokenRun).toHaveBeenCalledWith("@cf/test/model", expect.objectContaining({ max_tokens: 64 })); }); - it("treats invalid daily budget as zero budget", async () => { - const run = vi.fn(); + it("falls back to the HIGH shared default (10M) when the daily budget is invalid, like ai-review/ai-slop (#1369)", async () => { + const run = vi.fn(async () => ({ response: "Summary on the default shared budget." })); const env = createTestEnv({ AI: { run } as unknown as Ai, AI_SUMMARIES_ENABLED: "on", @@ -117,8 +118,28 @@ describe("Workers AI summaries", () => { const result = await summarizeAgentBundleWithAi(env, bundleFixture(), "private"); - expect(result).toMatchObject({ status: "quota_exceeded", remainingBudget: 0 }); - expect(run).not.toHaveBeenCalled(); + // A truthy-but-non-finite budget resolves to the 10M shared default (not the old 10k/zero starvation), + // matching the sibling AI features that share the same daily neuron counter. + expect(result).toMatchObject({ status: "ok" }); + expect(run).toHaveBeenCalled(); + }); + + it("resolves the SHARED neuron budget like ai-review/ai-slop: default 10M (not 10k) and ceiling 10M (not 1M) (#1369)", async () => { + // Default HIGH: with the budget unset and ~2M already used on the shared counter, summaries must still + // run — the old `|| 10000` default would have been quota_exceeded long before 2M. + const defaultRun = vi.fn(async () => ({ response: "Within the 10M default." })); + const defaultEnv = createTestEnv({ AI: { run: defaultRun } as unknown as Ai, AI_SUMMARIES_ENABLED: "true" }); + await recordAiUsageEvent(defaultEnv, { feature: "ai_review", model: "m", status: "ok", estimatedNeurons: 2_000_000 }); + expect((await summarizeAgentBundleWithAi(defaultEnv, bundleFixture(), "private")).status).toBe("ok"); + expect(defaultRun).toHaveBeenCalled(); + + // Ceiling raised: a configured 2M budget with 1.5M used must NOT be quota_exceeded — the old + // clamp(2M, 0, 1M) = 1M ceiling would have starved it. + const ceilingRun = vi.fn(async () => ({ response: "Under the 2M configured budget." })); + const ceilingEnv = createTestEnv({ AI: { run: ceilingRun } as unknown as Ai, AI_SUMMARIES_ENABLED: "true", AI_DAILY_NEURON_BUDGET: "2000000" }); + await recordAiUsageEvent(ceilingEnv, { feature: "ai_review", model: "m", status: "ok", estimatedNeurons: 1_500_000 }); + expect((await summarizeAgentBundleWithAi(ceilingEnv, bundleFixture(), "private")).status).not.toBe("quota_exceeded"); + expect(ceilingRun).toHaveBeenCalled(); }); it("keeps public summaries disabled unless explicitly enabled and rejects unsafe public text", async () => { @@ -319,6 +340,20 @@ describe("optional deterministic-summary rewrite layer", () => { expect(run).toHaveBeenCalledWith("@cf/meta/llama-3.1-8b-instruct-fp8-fast", expect.objectContaining({ max_tokens: 256 })); }); + it("resolves the rewrite path's SHARED neuron budget like ai-review/ai-slop: default 10M, ceiling 10M, invalid → default (#1369)", async () => { + // Invalid (truthy non-finite) budget → 10M shared default, not the old 10k/zero starvation. + const invalidRun = vi.fn(async () => ({ response: "Invalid budget falls back to the 10M default." })); + expect((await rewriteSignalBundleWithAi(publicEnv({ AI_DAILY_NEURON_BUDGET: "not-a-number" }, invalidRun), rewriteReq())).status).toBe("ok"); + expect(invalidRun).toHaveBeenCalled(); + + // Ceiling raised: configured 2M budget with 1.5M used must NOT be quota_exceeded (old clamp to 1M would). + const ceilingRun = vi.fn(async () => ({ response: "Under the 2M configured budget." })); + const ceilingEnv = publicEnv({ AI_DAILY_NEURON_BUDGET: "2000000" }, ceilingRun); + await recordAiUsageEvent(ceilingEnv, { feature: "ai_review", model: "m", status: "ok", estimatedNeurons: 1_500_000 }); + expect((await rewriteSignalBundleWithAi(ceilingEnv, rewriteReq())).status).not.toBe("quota_exceeded"); + expect(ceilingRun).toHaveBeenCalled(); + }); + it("honors a custom model and output-token configuration", async () => { const run = vi.fn(async () => ({ response: "Custom-config summary." })); const env = publicEnv({ WORKERS_AI_SUMMARY_MODEL: "@cf/test/model", AI_MAX_OUTPUT_TOKENS: "128" }, run);