fix(admin): lift the bench when the operator fixes the shared provider

The health ledger records failures per account and provider, and since
background work leaves out a benched provider, an operator who fixed the
instance-wide provider in the admin AI settings still saw every account's
background work sit out the pause the broken setup had earned, up to a day.
Only a person's own provider settings and the Codex sign-ins forgot the
record.

A successful save of the admin AI settings now forgets what the ledger
recorded for admin-openai on every account and closes the status notes'
negative windows, the same reset a person's own save makes for theirs
(clearStatusNegativeWindows without an account covers every record). The
ledger comments that still said a benched row only deprioritises now say
what it does for background work.

Refs #1279
This commit is contained in:
Marc-André Bombeck committed 2026-10-25 00:42:20 +02:00
1 parent f1163d5efb
commit 402a4a546c
5 files changed
+72 -16

No files matched your search

@@ -16,6 +16,10 @@ vi.mock("@/lib/db", () => ({
upsert: vi.fn(),
},
user: { findMany: vi.fn().mockResolvedValue([]) },
providerHealth: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
insightStatusCache: {
updateMany: vi.fn().mockResolvedValue({ count: 0 }),
},
},
}));
@@ -217,6 +221,31 @@ describe("PUT /api/admin/ai-settings", () => {
statusCode: 429,
});
});
// #1279 — background work leaves a provider the health ledger benched out
// of every account's chain. A fixed operator provider must not sit out a
// pause the broken one earned, so a saved change forgets what the ledger
// recorded for it on every account, and lets their notes try again.
it("forgets the operator provider's recorded failures on every account", async () => {
upsertEcho();
await PUT(jsonReq({ model: "gpt-4o-mini" }));
expect(prisma.providerHealth.deleteMany).toHaveBeenCalledWith({
where: { providerType: "admin-openai" },
});
expect(prisma.insightStatusCache.updateMany).toHaveBeenCalledWith({
where: {
OR: [{ retryAt: { not: null } }, { failureStreak: { gt: 0 } }],
},
data: { retryAt: null, negativeReason: null, failureStreak: 0 },
});
});
it("forgets nothing when the change is refused", async () => {
await expect(
PUT(jsonReq({ baseUrl: "https://attacker.example/v1" })),
).rejects.toMatchObject({ statusCode: 422 });
expect(prisma.providerHealth.deleteMany).not.toHaveBeenCalled();
});
});
/**
+12
View File
@@ -8,6 +8,8 @@ import { encrypt, decrypt } from "@/lib/crypto";
import { checkRateLimit } from "@/lib/rate-limit";
import { annotate } from "@/lib/logging/context";
import { legacyPrivateHostReport } from "@/lib/ai/legacy-private-host-report";
import { forgetProviderHealth } from "@/lib/ai/provider-health-ledger";
import { clearStatusNegativeWindows } from "@/lib/insights/status-negative-window";
export const dynamic = "force-dynamic";
@@ -134,6 +136,16 @@ export const PUT = apiHandler(async (request: NextRequest) => {
details: auditDetails,
});
// #1279 — background work leaves out a provider the health ledger benched,
// on every account whose chain falls back on this one, and a status note
// waits out its negative window. A fixed operator provider must not sit out
// a pause the broken one earned, so what was recorded against it is
// forgotten on every account, as a person's own save does for theirs.
await Promise.all([
forgetProviderHealth({ providerType: "admin-openai" }),
clearStatusNegativeWindows(),
]);
const hasKey = Boolean(settings.adminAiKeyEncrypted);
return apiSuccess({