Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/services/ai-summaries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
43 changes: 39 additions & 4 deletions test/unit/ai-summaries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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",
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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);
Expand Down
Loading